你还不知道CIL?一个让你开发项目更有性价比的中间语言(1)

作者:   出处:互联网   2015-06-18 15:43:47   阅读:1

项目紧赶慢赶总算在年前有了一些成绩,所以沉寂了几周之后,小匹夫也终于有时间写点东西了。以前匹夫写过一篇文章,对CIL做了一个简单地介绍,不过不知道各位看官看的是否过瘾,至少小匹夫觉得很不过瘾。

 

AD:
 

 

 

项目紧赶慢赶总算在年前有了一些成绩,所以沉寂了几周之后,小匹夫也终于有时间写点东西了。以前匹夫写过一篇文章,对CIL做了一个简单地介绍,不过不知道各位看官看的是否过瘾,至少小匹夫觉得很不过瘾。所以决定写几篇关于CIL的文章,即和各位看官一起进行个交流,同时也是匹夫自己总结和巩固一下这些知识点。俗话说的好, 万事开头,Hello World ,那么作为匹夫总结CIL的第一篇文章,就从Hello World开始吧。当然,正式开始写CIL代码之前,们还有点闲话要说,那就是运行时的选择为何是它?为何是CIL?而CIL为何又是基于堆栈的?内存或者寄存器难道不是更理想的选择吗?

 

为何是CIL?

 

开始正文内容之前,匹夫带领大家先回顾一下《Mono为何能跨平台?聊聊CIL(MSIL)》的简要内容:首先,用C#写的代码被C#的编译器编译成CIL(当然除了C#还有很多其他的语言,比如VB等等),之后再有JIT编译器在程序运行时即时编译或者AOT(或者NGEN)进行提前编译将CIL代码编译成对应平台的机器码,最后运行在平台上的便是机器码。小匹夫在那篇文章中提过,首先将各种不同的语言都统一编译成CIL,再由CIL编译成各个平台的机器码是跨平台的基础。那么仔细想想,一定有人会提出这样的疑问,直接从C#编译到机器码,省略掉 多余 的中间语言,是不是也可行呢?这个问题的确值得讨论,同时也为了小匹夫接下来的文章师出有名,所以首先聊聊CIL的 合法性 (用必要性这个词也许更好)问题就成了匹夫写这篇文章的头等大事。

 

论据一:考虑下 性价比

 

首先提出们的论据一,那就是使用CIL这套体系对实现跨平台的开销要小的多的多。

 

引入一个 多余的 中间语言和两个编译器(C#----- CIL------ 机器码)听上去总是要比只使用一种编译器(C#-------- 机器码)的实现代价高的多,因为们的目的是C#代码能编译成机器能运行的机器码,显然一步到位是最直接有效的方式。相反,引入中间语言之后,们就需要实现两种语言的分析和编译,看上起的确多此一举。但如果们考虑到跨平台这个前提,就会发现中间语言是多么的重要。

 

假设你可以选择的语言有N种(比如C#, VB, F#, JScript .NET,Boo...),而们的目标平台有M种(win,mac,linux,ios,android...)。那么如果们采用最直接的编译方式,即从源代码直接编译成机器码,那么到底需要多少个编译器呢?

 

答案很直接咯:需要N*M种编译器。因为你需要为每一种语言针对每一个平台写一个编译器。

 

如果们采用了中间语言呢?

 

们只需要为N种语言写N种编译器,将它编译成CIL代码。再为M种平台写M种编译器,将上一步生成的CIL代码编译成M种平台的机器码。那么这次们到底需要多少编译器呢?

 

答案也很明显:需要M+N种编译器。

 

所以,采用中间语言要比直接编译代码的开销小的多得多。

 

论据二:实现的难度

 

假设,匹夫对硬件语言一窍不通(当然事实上是这样的。。。),但却具备一种分析源代码语义的特殊天赋(瞎掰的)。那么要实现从C#到各个平台机器码一步到位的编译,匹夫就要去啃各种目标芯片的说明,将C#代码转化成对应芯片的机器码。这听上去就像是一条不归路,因为你并不擅长这个领域而且工作量巨大,同时由于不擅长带来的隐患难以估量。

 

换言之,这个难度太大了。

 

但是如果们通过对C#进行语义分析,能十分容易的就生成一份和芯片无关的CIL代码,那么实现的难度相比直接从C#到机器码那可是大大的降低了。因为CIL语言本身就十分简单(至少匹夫这种粗人都能看懂),所以从源代码到CIL的编译器实现就十分容易。同时,也是因为CIL语言本身十分简单,所以从CIL到机器码的编译器也十分简单。

 

而且即便有新的平台出现,你也不需要为每种语言都写一个针对新平台的编译器,而只需要实现一个从CIL到新平台机器码的编译器就可以了。

 

所以可以看到,CIL中间语言的出现,大大降低了跨平台的实现难度。

 

《Mono为何能跨平台?聊聊CIL(MSIL)》这篇文章中,小匹夫也给各位列举了一些CIL的代码,同时做了一些解释,文中在介绍CIL不依托cpu的寄存器时写了这样一句话:

 

不错,CIL是基于堆栈的,也就是说CIL的VM(mono运行时)是一个栈式机。

 

那么不知道各位看官是否也有这样的疑问呢?那就是~~~~~~~

 

为什么是栈式机?直接放在内存中不好吗?

 

终于要聊聊小匹夫也觉得挺有趣的一个话题了。对啊,为什么CIL基于堆栈呢?那么们首先就来聊聊什么是 栈式机 。

 

假如让你来...

 

假如让你来设计一种机器语言,同时实现一个简单地加法功能,简单到什么程度呢?比如a+b等于c这样好了。那么思路是什么呢?

 

方案一:使用内存

 


add [a的地址], [b的地址], [结果的地址也就是c的地址]

 

当机器遇到add操作符时,它就会去寻找a的地址和b的地址这两个地址中存放的值,然后用balabala的方式将它们求和,并将结果存放在c的地址。

 

方案二:使用寄存器

 

当然匹夫也是一个学过汇编的汉子,也了解一点点单片机的知识,知道有一个叫做累加器的东西。累加器就属于寄存器了,它主要用来储存计算所产生的中间结果,最后将其转存到其它寄存器或内存中。所以使用累加器的思路也很简单,一开始将累加器设定为0,每个数字依序地被加到累加器中,当所有的数字都被加入后,结果才写回到主内存中。

 

方案三:使用堆栈

 

等等,这个部分介绍的不是栈式机吗?怎么感觉有点跑题呢?好吧,拉回思绪,让们再来考虑下使用堆栈如何实现这个简单地加法功能呢?

 

push a

push b
pop c

 

add操作符首先将a,b弹出堆栈,然后将二者相加,再将结果压栈。那么,使用了这种方案的虚拟机,就被称为 栈式机 。

 

所以如果要回答为何CIL的选择是使用堆栈,那么就绕不过堆栈和另外两种方案的比较。

 

首先看一下们做这种简单加法时,硬件需要为们提供一些什么呢?对,就是存放这些值的临时空间。所谓的临时空间,就是说存储这个值的空间只有在需要这个值的时候才有用,其余的时候你并不需要关心这个空间或者说它的地址到底是什么。假设们已经定义了一些操作符,比如Allocate用来分配内存,Call用来调用函数,Add用来求和,Store则是用来存储数据。

 

首先们直接使用内存来运行CIL,那么遇到这样的表达式:

 


x = A() + B() + C() + 100

 

机器首先要为A()在内存上分配空间用来保存它的返回值,然后调用A()并将A()的返回值保存在之前分配给它的地址中,们就管它叫做ret1好了。之后为B()在内存上分配空间来保存B()的返回值,接着调用B(),同样将B()的返回值保存在刚才分配给它的内存中,们暂时称呼它ret2。这时,们遇到了第一个 + 号,所以此时会为ret1和ret2相加的结果在内存上分配一个空间,并且将ret1和ret2相加,并将结果保存在刚刚分配的内存中(们称为sum1),之后的过程以此类推。


 
 

Allocate ret1  //为A()的返回值分配临时空间ret1  Call A(),ret1 //调用A()并将结果保存在ret1  Allocate ret2   //为B()的返回值分配临时空间ret2  Call B(),ret2 //调用B()并将结果保存在ret2  Allocate sum1 //为第一次相加的结果分配临时空间sum1  Add ret1,ret2,sum1 //使用Add操作符将ret1和ret2中的内容相加,并将结果保存在sum1中。  ... 

 

可以看到这样的CIL代码在每一步真正的逻辑执行之前,都会先在内存上分配一块临时空间,用来存储们此时需要的数据。如果使用堆栈,这个步骤是不需要,因为你将你需要的数据存储在了堆栈之中,而非在内存上临时去分配空间。所以,使用堆栈时,CIL代码看上去也许像是这样的:


 
 

push x的地址 // 将x的地址压栈  call A() // 现在堆栈中包含x的地址和A()的返回值ret1  call B() // 现在堆栈中包换x的地址,ret1,B()的返回值ret2  add // 现在堆栈中包含x的地址,ret1 + ret2的结果sum1  call C() // 现在堆栈中包含x的地址,sum1和C()的返回值ret3  add // 现在堆栈中包含x的地址, ret1+ret2+ret3的返回值sum2  push 100 // 现在堆栈中包含x的地址,sum2,以及100  add // 现在堆栈中包含x的地址, ret1+ret2+ret3+100的和sum3  store //将sum3存在x的地址中。 

 

同时,们还可以看到如果CIL直接使用内存的话,由于在内存上的空间是临时分配的,所以CIL代码在运行时需要带上它的操作数地址以及返回地址,比如上例中的Add ret1,ret2,sum1,因为如果不告诉它这些地址,它就不知道该从何处得到数据,并将返回的数据放在何处。

 

所以直接使用内存来运行CIL代码,会使得CIL代码变得十分的臃肿不堪,而且要做很多多余的工作。所以不直接使用内存,而是使用堆栈的原因就是因为:如果们仅仅只是为了临时存储一些值,而在使用完这些值之后们就不再关心这块空间如何如何,显然使用堆栈要比直接使用内存方便的多,简洁的多。

 

至于为何不使用寄存器,小匹夫在上文提到的文章中已经解释过了。简单的讲就是因为简单。

 

好啦,到此为CIL正名的过程就结束啦。那么下面就开始首尾呼应,结尾点题,从Hello World开始踏上们的CIL语言的征程吧~~

 

 


1 2 下一页 查看全文
内容导航 第 1 页:为何是CIL?  第 2 页:Hello Wolrd ,沃尔德



2006软考上半年试题分析与解答
本书是针对全国计算机技术与软件专业技术资格(水平)考试而编写的,书中详尽分析与解答了2006年上半年的程序员级、软件设计师级

Copyright© 2006-2015 ComponentCN.com all rights reserved.重庆磐岩科技有限公司(控件中国网) 版权所有 渝ICP备12000264号 法律顾问:元炳律师事务所
客服软件
live chat