CLR_via_C#.3rd 翻译[1.4执行程序集代码]

1.4 Executing Your Assembly’s Code 执行程序集代码

 

如前所述,托管程序集同时包含元数据和IL。 IL是由Microsoft在咨询了一些商业和学术上的语言编译器作者后创建的一种独立于CPU的机器语言。 IL是一种比大多数CPU机器语言更高层次的语言。IL可以访问和操纵的对象类型,创建和初始化对象,调用对象的虚方法,操作数组元素。它甚至能抛出和捕获异常的错误处理。你可以认为IL一种面向对象的机器语言。 

 

通常,开发人员会使用一门高级语言编程,如C#和C++/ CLI中,或Visual Basic。这些高级语言的编译器产生IL。然而,正如任何其他机器语言,IL可以用汇编语言编写,微软也确实提供了IL汇编器,ILAsm.exe。微软还提供了IL反汇编,ILDasm.exe。 

 

请记住,任何高级的语言最多只能使用CLR全部特性一个子集。而IL汇编语言允许开发人员获取CLR的所有特性。因此,如果你真的想使用被你的编程语言隐藏掉的CLR特性,你可以在IL汇编器中编写或者使用另一种编程语言来完成你需要的CLR功能。 

 

了解CLR提供了哪些功能的唯一途径就是阅读有关CLR的文档。本书重点讲述CLR的一些特性,以及它们在C#语言中哪些是提供的,哪些是没有提供的。我想很多其他的书籍和文章都会从一门语言的角度去探讨CLR,并且大多数开发人员也倾向于相信CLR仅提供了他选所选择的语言所展现的功能。如果大家选额的语言能够实现想做的事情,那么这种理解也并非坏事,虽然有些混乱。

 

重点:我觉得这种语序在编程语言间方便切换和高度集成的能力是CLR一个非常厉害的功能(an awesome feature of the CLR)。不行的是,许多开发人员经常会护士这个特性。我们知道,C#Visual Basic善于进行I/O操作,而APL在高级工程和金融计算时则非常突出。通过CLR,我们可以用C#来编写应用程序的I/O部分,然后再用APL来编写应用程序的工程计算部分,CLR提供的语言之间的继承能力是前所未有的,这也是的混合编程值得很多开发项目考虑。

 

在执行一个方法的时候,它的IL首先要转化成当地的CPU指令。这是CLR中(JIT--just-in-time)编译器的工作。

1-4展示了一个方法第一次被调用时的情况

 

 

在Main方法执行之前,CLR会检查Main中代码引用到的所有类型。这会导致CLR分配一个内部的数据结构,该数据结构用于管理对所引用到的类型的访问。在图1-4中,Main方法只引用了一个Console类型,CLR将会为此分配一个单独的内部结构。在这个内部数据结构中,Console类型中的每个方法都有一个条目(entry),每个条目中将保存有一个方法实现代码的地址。当初始化这个结构时,CLR将把每一个条目设置为CLR内部的一个没有正式记录的函数,我们暂且成该阐述为JITCompiler

 

当Main方法第一次调用writelineJITCompiler函数将被调用,该函数负责将一个方法的IL代码编译成本地CPU指令。因为IL代码是被“即时(just-in-time)”编译的,所以CLR的这一部分通常被称作JITter或者JIT编译器。

 

Note如果应用程序是在x86版本的windows上或者是WoW64上跑的,JIT编译器会产生x86指令。如果应用程序是在x64版本的windows或者Itanlum版本的windows上跑,JIT编译器会分别产生x64或者IA64指定。

 

当JIT编译器(JITCompiler)函数被调用时,它会知道正在调用的是那个方法,以及该方法是由那个类型定义的。JIT编译器函数随后会在被调用方法所定义的程序集中的元函数内搜索其IL代码的位置。JIT编译器接着验证这些IL代码并将编译成本地CPU指令。本地CPU指令会被保存在动态分配的内存快中。然后JIT编译器将前面内部数据结构中被调用方法的地址替换成包含本地CPU指令的内存块地址。最后,JIT编译器会跳转到该内存块中的代码上。这里的代码就是Writeline方法(含有一个String类型参数的版本)当这些代码执行完,它将返回到Main函数中,Main函数会接着继续执行下面的代码。

 

现在,Main第二次调用WriteLine。由于writeLine已经被验证以及编译过了,所以这次将直接调用到内存快,完全跳过了JIT编译器函数。WriteLine执行完后,同样返回到Main

 

 

这样,一个方法只有在被首次调用时才会产生一些性能损失。所有对该方法后续的调用都将以本地代码做全速执行,因为本地代码不再需要验证和编译。

 

JIT编译器将本地存储在动态内存之中。这以为这当应用程序关闭时,编译生成的本地代码将被丢弃。这样,如果我们以后再次运行该应用程序,或者同时运行该应用程序的两个不同实例,JIT编译器需要再次将同样的IL代码编译成本地指令。

 

对于大多数的应用程序,JIT编译引起的损失是微不足道的。而且,大部分的也能够用程序经常反复调用同一个方法。这样,在应用程序执行时,这些方法引起的也只是一次性能损失。而且通常方法内部执行所花费的时间要比方法调用本身索花费的时间要多的多。

 

你还要知道CLRJIT编译器可以像非托管C++编译器后端一样优化本地代码。另外,产生优化代码可能花费更多的时间,但是经过优化后的代码的执行效率显得会更高。

 

有两个C#编译器选项会影响代码的优化:optimizedebug。下面的表格将向我们展示我们这些选项在 C#编译器生成IL代码、和JIT编译器生成本地代码的质量上的影响。

 

 

选择/optimize-,由C#编译器产生的未优化的IL代码包含了很多无操作(no-operation NOP)的指令,并分支跳转到下一行代码。这些指令是为了能够在调试的时候使用Visual Stidio中的编辑-继续功能,还有一些额外的指令也能够使断点调试(比如一些控制流程的指令:for , while ,do ,if ,else, try ,catch 还有finally 语法块)更方便。当生产优化的IL代码,在C#编译器将通过删除多余的NOP和分支指令。此外,某些功能可能无法正常工作的评价时,调试器内进行。但是,IL代码比较小,使产生的EXE / DLL文件更小,使我们这些想了解IL是如何产生的更容易。

此外,只有当你选择了/debug(+/full/pdbonly)编译器产生一个程序数据库(PDB)文件。PDB文件可以帮助调试器查找本地变量,并将IL指令映射到源代码。在/debug:full选择下,JIT编译器被告知将会调试程序集,JIT编译器会找到每句IL指令的本地代码。您可以使用Visual Studio的JIT调试器(JIT Debugger)的调试功能来调试。没有了/debug:full,JIT编译器不会这么做,默认情况下,不会跟踪IL到本机代码信息,使得JIT编译器运行更快一点,使用的内存更少些。如果您启动Visual Studio调试器的进程,它迫使JIT编译器跟踪IL到本地代码的信息(无论/ debug开关),除非你关闭VisualStudio中的在加载模块禁止JIT优化(只限于托管)选项。

当你在Visual Studio创建一个新的C项目,该项目的调试(Debug)配置是/optimize-和/debug:full选项,发布(Release)的配置/optimize+/debug:pdbonly

 

对于那些有非托管CC++背景的开发者,可能会对这里的一些性能差别有所顾虑。毕竟,非托管代码是针对某一特性CPU平台所变异的,当他们被调用是,这些代码便会立即执行。而在托管环境中,代码的编译要经过两个阶段才能完成。首先,编译器要所秒源代码,把它编译成IL代码,但是要执行这些IL代码,它们还必须在运行时被编译成本地CPU指令,这项工作通常需要分配更多的时间。

 

相信我,我也是从C/C++过来的,我对这些额外的开销也非常关心,也有过相当的怀疑。无可争议,出现在运行时的第二阶段编译不仅会损伤系统性能,还要分配额外的动态内存。但是微软针对心跟那个已经做了很多工作,开销已经被降到最低。

 

你可能不会相信,托管应用程序有可能在性能方面超过非托管应用程序,但很多人(包括我)都认为这是有可能的。这里有很多原因。举例来说,当JIT编译器在运行时将IL代码编译成本地代码,编译器对于执行环境的了解要多余非托管编译器。下面是托管代码有可能超过非托管代码的一些地方:

JIT编译器能监测到一个应用程序是否运行在一些新型的CPU(奔腾4CPU)上运行,并产生利用这些新型CPU提供的特殊指令的本地代码。而非托管应用程序通常被编译为面向具有最小通用功能集合的CPU平台,一般会避免使用新型CPU提供的特殊指令。

JIT编译器能监测到正在运行的机器上某些总是返回错误的布尔测试。例如,考虑有如下代码的一个方法if (numberOfCPUs > 1){........}

如果宿主机器只有一个CPU,那么对于该段代码,JIT编译器将不会产生任何CPU执行。在这种情况下,针对宿主机器的本地代码机会得到更好的调整;代码将变得更小,执行速度也会更快

●在应用程序运行时,CLR能够分析评估代码的执行情况,并有选择的重新将IL代码编译为本地代码。根据观察到的执行模式,被编译的代码可以被重新组织以提高分支预测的成功率。当前的CLR并不支持这个功能,但是将来的版本会的。

 

如果你经验告诉你CLRJIT编译器不能为应用程序提供期望的性能,那么你可以使用NGen.exe工具,它是和.NET框架SDK一起发布的一个工具。该工具可以将一个程序集中的所有的IL代码转化为本地代码,并将结果代码保存在磁盘上的一个文件中。在运行时程序集被加载的时候,CLR将自动检查是否有该程序集存在,如果存在,CLR将加载预编译的代码,不再需要额外的运行时编译。注意NGen.exe对于实际执行环境的假设肯定比较保守,因此NGen.exe产生的代码将不如JIT编译器产生的代码那样高度优化。

原文地址:https://www.cnblogs.com/TivonStone/p/1807155.html