谈IL(1):IL是什么,它又不是什么?那么汇编呢?
我们.NET开发人员必定离不开IL,就算您没有学习,也一定可以在各处看到它的身影。最近在博客园上活跃的IL文章译者包建强同学的一些看法让我大为震惊,决定独立开篇,希望可以让大家看到不同的声音。真理越辩越明,也欢迎大家来一起讨论,发表自己意见。我也会尽量把朋友们留在我博客上的看法汇总起来,并加以回应。
《我谈IL》也是系列文章,目前的计划有4篇,依次是:
IL是什么,它又不是什么?那么汇编呢?。
CLR内部有太多太多IL看不到的东西,包括您平时必须了解的那些(示例)。
IL可以看到的东西,基本上都可以使用C#等高级语言来发现(示例)。
什么时候应该学IL,该怎么学IL。
您现在看到的便是本系列的第1篇:IL是什么,它又不是什么?那么汇编呢?。
我曾经在博客《浅谈尾递归的优化方式》和《使用WinDbg获得托管方法的汇编代码》涉及到了一些x86汇编,包同学在文章后留言,认为通过UltraEdit32此类编辑器观察到x86汇编:
我第三次仔细看了一遍最后这段代码,这段代码应该是从UltraEdit32中看到的,所以里面很多word不是IL中的关键字……
……就像我昨天提到用UltraEdit32这个工具来解读汇编代码,道理是一样的……
同样的事情发生在我最近写的一篇文章《从汇编入手,探究泛型的性能问题》以及包同学自己的文章后面的回复中:
如果不熟悉IL,又怎么能自己动手分析性能呢?你这篇文章不就证明了学习一点IL的重要性了么?
其实IL就是一门汇编语言,很奇怪有人一边在用ILAssembler分析性能,一边又在讲不要学习IL的话……
包同学作为微软MVP,是许多IL文章的译者,还有一本译作《Expert.NET2.0ILAssembler》即将出版,本应是这方面的专家。但是我现在非常担心这位专家的话会给许多学习.NET的朋友们一些较为严重的误导。从包同学之前的话来看,他对于IL和汇编的概念,从各自的作用到获取方式几乎完全混淆起来。因此冲动的我每次看到这样的言论都忍不住跳出来批驳一番,而这次更决定独立成文进行详细说明。
IL是微软.NET平台上衍生出来的一门中间语言,.NET平台上的各种高级语言(如C#,VB,F#)的编译器会将各自的文字表述方式转化为IL。各种不同的文字形式最终被统一到了IL的表述方式,其中包含了.NET平台上的各种元素,如“范型”,“类”、、“接口”、“模块”、“属性”等等。值得注意的是,各种高级语言本身可能根本没有这些“概念”在里头,如IronScheme是一个在.NET平台上的Scheme语言实现,其中根本没有前面提到的这些IL——亦或说是.NET平台上的名词。IL本身并不知道自己是由哪种高级语言转化而来的,哪种语言中有哪些特性,IL也根本不会关心。
谁来关心这些呢?自然是各语言的编译器了。这就是.NET平台上的高级语言的第一次转化:高级语言=>IL。
而我们平时说的“汇编”则要简单得多,这个简单并不代表“容易掌握,方便使用”,这个“简单”是指它的“定义”。汇编是让CPU直接使用的“语言”,请注意“直接”二字:一条汇编指令便是让CPU作一件事情(如寄存器的复制,从内存中读取数据等等),毫无二义。不同族CPU拥有不同的指令集,但是它们都有一样的特征:指令的数量相对较少,每个指令功能都简单之至。这也是为什么现在几乎没有人直接使用汇编写程序的原因,试想一下给您红、绿、蓝三原色,让您绘制一幅色彩绚丽的图画有多么困难。
由于CPU只认识汇编代码,因此就算是IL也需要再次进行转化,才能被CPU执行。这次转化便由“JITCompiler”完成。CLR加载了IL之后,当每个方法——请注意这是IL中的概念——第一次被执行时,就会使用JIT将IL代码进行编译为机器码——对了,刚才我忘了提,机器码和汇编其实也是一一对应的,您可以这样理解:汇编是机器码的文字表现形式,提供了一些方便人们记忆的“助记符”。与IL不同的是,CLR,JIT都是真正了解CPU的,对于同样的IL,JIT会把它为不同的CPU架构(如x86/IA64等等)生成不同的机器码。这也是Java/.NET中“CompileOnce,RunEverywhere”这一口号的技术基础:它们为不同的CPU架构提供了不同的“IL转化器”,仅此而已。与高级语言到IL的转化类似,CPU也完全不知道自己在执行的指令是从哪里来的,可能是JIT从IL转化而来,可能是JVM从JavaBytecode转化而来,也有可能是C语言编译得来,也有可能是由MIT/GNUScheme解释而来。
这就是.NET平台上的高级语言在机器上运行的第二次转化:IL=>汇编(机器码)。
因此,IL和汇编的区别是显著的。IL拥有各种高级特性,它知道什么是范型,什么是类和方法(以及它们的“名称”),什么是继承,什么是字符串,布尔值,什么是User对象。而CPU只知道寄存器,地址,内存,01010101。与汇编相比,IL简直太高级了,几乎完全是一个高级语言,比C语言还要高级。因此,您会看到.NETReflector几乎可以把IL代码“一五一十”地反编译为可读性良好的C#代码,包括类,属性,方法等等;而从汇编只能勉勉强强地反编译为C语言——而且其中的“方法名”等信息已经完全不可恢复了,更别说“模块”等高级抽象的内容。您想要把汇编反编译成C#代码?相信在将来这是可行的,不过现在这还是天方夜谭。
那么我们再来看看包同学的观点,例如首先他认为“用UltraEdit32这个工具来解读汇编代码”。如果您理解了我之前的解释,应该可以意识到这完全是一种谬论。IL是一种高度抽象,在运行之前,还需要由JIT转化为机器码才行。同样的IL代码,可以由不同CPU架构下的JIT编译成不同的机器码(同样的IL代码在同样的机器上是否也生成同样的机器码呢?答案是否定的,例如“泛型”……下一篇文章中我们会对此进行观察)。甚至于,CLR在运行了一段时间之后,可以让JIT重新生成一段更适合当前环境,性能更高的机器码供CPU执行。从这个角度上说,IL是静态的,而汇编是动态的。设法使用一个静态查看工具UltraEdit32来阅读一个动态的,不确定的内容,这又该如何实现呢?
不过真要说起来,使用UltraEdit32从理论上的确可以阅读一个编译后的IL代码,因为此时IL已经以二进制的形式存储在程序集文件中。例如ILDisassembler(ildasm.exe)和.NETReflector便是通过读取程序集文件的数据(而不是在将程序集加载到CLR中)来获得IL代码,微软也发布了CommonCompilerInfrastructure:Metadata和CCI:CodeandAST两个和.NET基础结构有关的开源项目。而近在“博客园”中,也有AndersLiu大牛写过一个CliPeViewer,从程序集的物理结构中读取其元数据,再进一步便可获取到IL代码了。
虽然已经有了多次请求,但是包同学还是没有公布他使用UltraEdit32获取汇编代码的做法。窃以为,是因为“这个真无法实现”的原因吧。
至于包同学的另一个看法是,我使用ILAssembler(他应该是指ILDisassembler)查看汇编代码所犯的错误同样是混淆了IL和汇编两种截然不同的东西。那些都是我在程序运行之后,使用WinDbg观察汇编的结果,也就是说,是JIT将IL进行再次编译(第一次是指高级语言编译器生成IL)的结果。由于JIT每次进行处理的最小单元是“方法”,因此如果一个.NET方法还没有执行过,则是无法获取它的汇编代码的。我在《使用WinDbg获得托管方法的汇编代码》一文中清楚地演示了目标方法在BeforeJIT和AfterJIT两个不同情况下,由WinDbg观察到的结果。
由于IL还是过于高级,因此很多真真切切的东西并无法看到,因此我也不得不用汇编从根本上证实了泛型不会降低性能——严格来说,由于IL在不同平台上生成的汇编不同,我其实也只是证实了在x86平台上的这个结论。请您回想一下,您看过的.NET书籍中有多少是使用IL来说明问题的呢?为了心中踏实,我刚才又去翻了翻《Essential.NET》、《CLRviaC#》这《Customizingthe.NETCommonLanguageRuntime》这几本“偏底层”的书,证实了这一观点。其实理由很简单,一是大师能够用朴实的语言,或更易理解的高级代码把问题明明白白彻彻底底地讲清楚,二便是因为IL可以说明的东西实在有限。例如,泛型在运行期间是否生成了新类型?CLR对待引用类型和值类型的泛型时是否一样?
在包同学之前的文章中,横道天笑也回复了类似的看法:
……我觉得IL的作用就仅此而已,对于其他,甚至是虚方法调用等等从IL上都看不出来什么。因为你真的不知道callvir指令内部到底干了啥,更别说性能了,因为IL之后还有个JIT,JIT之后才得到的是我们传统上的汇编,汇编语言与机器指令是一一对应的,所以从汇编语言上才能发现一切的一切,比如多态靠的是对象地址加偏移,比如我的那篇汇编讨论性能的文章。
这篇文章的目的是“讲道理”,在接下来了两篇文章中,我还会通过示例,也就是“摆事实”来解释为什么说“CLR内部有太多太多IL看不到的东西,包括平时您必须了解的那些”,以及“IL可以看到的东西,基本上都可以使用C#等高级语言来发现”。
最后,为了避免给大家带来误导,我还是希望多补充几句。我的这几篇文章似乎是在“呼吁”大家不要学习IL,其实不然。我是希望在澄清事实的的基础上,探究出一些更有实践价值结论。例如学习IL和汇编有什么好处,什么是对IL和汇编的滥用,以及究竟该如何可以更快速方便地解决某些问题等等——例如我使用汇编来说明泛型的性能问题就是一种滥用,最好的方法应该是“编写试验代码”。
因此,本系列文章的最后一篇想讨论的话题便是“什么时候应该学IL,该怎么学IL”。在这里,我也请大家留下您的观点,真理便是在各种观点中总结出来的,不是吗?
?
看了大家的评论,我觉得可能还需要把汇编和机器码的关系进行一些补充。
有朋友提出,汇编不是和机器码一一对应的,因为还有宏汇编,高级汇编等等。其实高级汇编其实已经是一种“语言”了,它最终还是被转换成机器码执行。我说的汇编是指汇编“指令”,由机器码简单一一替换成助记符给人看。
还有某位朋友指出的一点,如果JIT生成的是汇编,那么肯定无法给机器直接执行。文章里可能没有说清楚,我的意思是,JIT生成机器码,而汇编是机器码进行简单替换后,方便给人看的结果。阅读汇编代码,就完全了解了CPU是如何一步一步进行执行任务的,没有丝毫偏差。这也是我认为观察汇编就是阅读机器码的原因。
Font:ComicSansMS
谈IL(2):CLR内部有太多太多IL看不到的东西,包括您平时必须了解的那些
我一直建议大家不要倾向于学习IL的原因有二:
IL能够说明的内容太少,包括大部分.NET“必知必会”。
IL能够获得的信息从高级语言中也大都可以知道。
而这篇文章便是希望通过实例来把第1点解释清楚,而第2点则留给下一篇文章来解释。
在文章开始之前,我先要承认两个错误:
首先,上一篇文章对于“IL”和“汇编”的阐述还有些混淆。在这方面某些朋友给出了一些更确切地说法,IL是一种为.NET平台设计的汇编语言,拥有大量.NET平台中特有的高级特性。而x86汇编等则是与机器码一一对应的文字形式代码。不过为了方便表述,在这一系列文章中,还是以“IL”来指代.NET平台上的中间语言,以“汇编”来指代x86汇编这种和特定CPU平台紧密相关的事物——包括之前那篇文章,其实是在阐述IL和汇编之间的关系和区别,以及该如何对待它们的问题,而并非为IL是否可以被叫做是“汇编”进行争论。
其次,在第1篇文章发布的时候,现在这篇文章,也就是本系列第2篇的标题是“汇编可以看到太多IL看不到的东西”。不过后来在半夜重读这篇文章,并且仔细整理这篇文章的示例时发现出了一个问题——我并不是在讲汇编,要探索CLR中的各种问题也并不是仅仅靠汇编来发现的。当时写文章的时候谈了太多的IL和汇编,最终把自己的思路也给绕了进去。现已修改,希望没有给朋友们造成误解,抱歉。今后我也会尽量避免此类情况发生。
好了,现在开始继续我们这次的话题。
既然是讨论IL,自然要讨论IL可以做什么,这样才能够以此得出IL究竟该不该学,如果该学的话有应该怎么学——这话当然是对你,我,他个人来说的,不可一概而论。不过也有一个东西需要了解的,就是IL到底表达出了什么,IL又没有表达出什么东西。
在这里继续强调一下上文的结论:无论IL是否算是一种“汇编”,都不影响它是一种非常高级的编程语言,拥有各种高级特性,例如泛型、引用类型,值类型,方法属性等等,这些特性才是我们判断的依据,而不是它是否能够被冠上“汇编”的“称号”。简单地说,我们应该看它“能(或不能)做什么”,而不是它“能(或不能)被叫做什么”。以下就通过几个示例来展示一些情况:
示例一:探究泛型在某些情况下的性能问题
为了契合本文的内容,也为了说明问题,我先举一个例子,那正是在《从汇编入手,探究泛型的性能问题》一文中使用过的例子:
namespaceTestConsole
{
publicclassMyArrayList
{
publicMyArrayList(intlength)
{
this.m_items=newobject[length];
}
privateobject[]m_items;
publicobjectthis[intindex]
{
[MethodImpl(MethodImplOptions.NoInlining)]
get
{
returnthis.m_items[index];
}
[MethodImpl(MethodImplOptions.NoInlining)]
set
{
this.m_items[index]=value;
}
}
}
publicclassMyList
{
publicMyList(intlength)
{
this.m_items=newT[length];
}
privateT[]m_items;
publicTthis[intindex]
{
[MethodImpl(MethodImplOptions.NoInlining)]
get
{
returnthis.m_items[index];
}
[MethodImpl(MethodImplOptions.NoInlining)]
set
{
this.m_items[index]=value;
}
}
}
classProgram
{
staticvoidMain(string[]args)
{
MyArrayListarrayList=newMyArrayList(1);
arrayList[0]=arrayList[0]??newobject();
MyList |
|