1.4 执行你的程序集代码

前面提到, 托管的程序集包含着metadata和中间语言(IL), IL是一个独立于CPU的机器语言, 是微软与几家外部商业和学术的语言/编译器作者协商之后开发的. IL是比绝大多数CPU机器语言高级的语言, IL能够访问和操作对象类型, 能够创建和初始化对象, 调用对象的虚函数, 能直接操作数组元素, 它甚至还能为错误处理抛出和捕获异常. 你可以把IL想象为一个面向对象的机器语言.

通常, 开发者编写高级语言, 例如C#,C++/CLI, 或者VB. 为这些高级语言工作的编译器将产生IL. 然而, 像其它机器语言, IL可以用汇编语言来编写, 微软确实提供了一个IL汇编器, ILAsm.exe, 还提供了一个IL反汇编器ILDasm.exe.

记住任何层次的语言很可能只暴漏出CLR提供的一部分功能. 然而, IL汇编语言允许开发者访问所有的CLR功能. 因此, 如果你选择的编程语言隐藏了部分CLR的功能, 你可以用IL汇编语言编写那部分代码, 或者利用其他具有你需要的CLR功能的编程语言.

获得CLR提供的功能的唯一方式是阅读CLR文档, 在本书, 我尽力关注CLR功能, 以及如何被C#语言暴露的(或没有暴露). 我怀疑其他书籍或者文章只是通过一种语言来阐述CLR的功能, 使得大多数开发者理解CLR只提供了那种语言所对应的那部分功能. 只要你的语言允许你完成你想做的事情, 那么这种模糊的看法也不是件坏事.

 重要: 我认为这种方便在语言之间进行切换和切换的能力是CLR一个很强大的功能. 不幸的是, 我也认为开发者将会更加忽视这个特性. C#VB这样的编程语言对于执行I/O操作是非常优秀的语言. APL对于执行高级工程或商务计算是非常优秀的语言. 通过CLR, 你可以用C#编写你的应用程序的I/O操作部分, APL编写工程计算的部分. CLR提供了这些语言之间集成的层次, 这是前所未有的技术, 这使得混合语言编程值得许多开发项目考虑.

为了执行一个函数方法, 它的IL必须被转换成native CPU指令, 这是CLR JIT(just-in-time)编译器的工作.

1-4给出了一个函数方法第一次被调用所发生的事情.

1-4 第一次调用一个函数

在执行Main函数之前, CLR检测被Main的代码所引用的所有类型, 这导致CLR分配内部的数据结构用于管理被应用类型的访问. 在图1-4, Main函数引用一个类型, Console, 导致CLR分类一个内部结构, 这个内部数据结构为Console类型定义的每个方法包含一个条目, 每个条目包含方法实现的地址. 当初始化这个结构时, CLR设置这些条目到一个内部的没有文档描述的函数(CLR内部), 我们称这个函数为JIPCompiler.

Main函数第一次调用WriteLine, JITCompiler函数被调用, JIPCompiler函数负责编译一个函数的ILnative CPU指令. 因为IL是在”just in time”时编译的, 这个CLR组件也常称为JITter或者JIT Compiler.

注意: 如果应用程序运行在x86版本的Windows或者WoW64, JIT Compiler将产生x86指令, 如果应用程序以64位应用程序运行在x64或者IA64版本的Windows, JIT Compiler将产生x64或者IA64指令.

在调用时, JITCompiler函数知道正在调用的是什么函数, 知道这个函数的类型. JITCompiler函数然后在程序集的metadata中搜索被调用方法的IL. 然后JITCompiler验证和编译IL代码为native CPU指令, native CPU指令被保存在动态分类的内存块中. 然后JITCompiler返回到被调用函数的条目上(CLR创建的类型的内部数据结构), 并替换那个引用为包含刚才编译为native CPU指令的内存块的地址. 最后, JITCompiler函数跳到内存块的代码处, 这个代码就是函数WriteLine的实现(接受String参数的版本). 当这个代码返回时, 它返回到Main, 然后继续正常执行.

Main现在第二次调用WriteLine函数, 这次, WriteLine的代码已经被验证和编译了, 因此调用直接转到内存块, 完全跳过了JITCompiler函数, WriteLine函数执行完之后, 它就返回到Main函数. 1-5给出了第二次调用WriteLine函数的过程. 

1-5 第二次调用一个方法的过程

函数只有在第一次调用时有性能上的损失, 随后的调用是全速执行native代码, 因为不需要对native代码进行验证和编译.

JIT compilernative CPU指令存储在动态内存中, 这意味着被编译的代码在程序终止时将被抛弃, 因此如果你再次运行应用程序, 或者同时运行两个应用程序实例(在两个不同的操作系统进程), JIT compiler将再次将IL编译为native指令.

对大多数应用程序来说, JIT编译所导致的性能下降不是很重要, 多数应用程序常常是重复调用相同的函数, 这些函数在应用程序执行时只有一次性能上的下降, 容易看出花在函数内部的时间比调用的时间长的多.

你应该也注意到CLRJIT编译器优化了native代码, 这类似于非托管的C++编译器在后端所作的工作, 它可能需要更多的时间来产生优化的代码, 但是优化过的代码比没有被优化的代码执行的更快.

注意: 有两个C#编译器开关能够影响代码的优化: /optimize/debug, 下面的表格给出了这些开关对C#编译器产生的IL代码和JIT编译器产生的native代码的影响.

编译器开关设置

C# IL代码质量

JIT native代码质量

/optimize- /debug-

(默认)

未优化

优化

/optimize- /debug(+/full/pdbonly)

未优化

未优化

/optimize+ /debug(-/+/full /pdbonly)

优化

优化

对那些来自非托管C或者C++背景的开发者来说, 你可能会考虑到这导致的性能后果, 毕竟, 非托管的代码是为特定的CPU平台编译的, 当被调用时, 代码可以简单地执行. 在这种托管的环境下, 编译代码有两个阶段, 第一, 编译器处理源代码, 做尽可能多的工作来产生IL, 但是为了执行代码, IL自己还要在运行时被编译为native CPU指令, 这需要更多的内存分配, 需要额外的CPU时间来完成这件事.

相信我, 因为我也是从C/C++背景走过来的, 我也非常怀疑和关注这种额外的开销. 事实是第二个编译阶段不会损害性能, 它是分配动态的内存. 然而, 微软已经做了很多改进性能的工作来将这个额外的开销降到最低.

注意: 当产生未优化得IL代码时, C#编译器将在代码中产生NOP(控操作)指令, NOP指令使得调试时能够使用edit-and-continue功能, 这些NOP指令也使得代码更容易被调试, 可以在控制流程指令(for, while, do, if, else, try, catch, and finally)上设置断点. 这被假设为一种功能, 但是我也发现这些NOP指令有时很令人烦恼, 因为我必须单步跳过它们, 当我调试某些代码时, 这会减慢我的工作. 当产生优化的IL代码时, C#编译器将去掉这些NOP指令.

注意当JIT编译器产生优化的native代码时, 代码在调试器中将会很难单步执行, 控制流程会被优化. 一些函数评估可能不会工作.

当你在Visual Studio中创建一个新的C#项目时, 项目在调试配置时具有/optimize-/debug:full, 项目在Release版本的配置时具有/optimize+/debug:pdbonly.

不考虑这些设置, 如果一个调试器被附到一个包含CLR的进程上时, JIT编译器总会产生未优化的native代码来帮助调试, 当然代码的性能会受到影响.

如果你也是怀疑论者, 你应该build一些应用程序来测试性能, 此外, 你应该运行一些微软或者其他公司提供的不平凡的托管的应用程序, 然后评价其性能. 我认为你将会看到性能是如何的优秀.

你可能发现这难以置信, 但是很多人(包括我)认为托管的应用程序实际上会比非托管的应用程序高效, 有很多原因. 例如, 当在运行时JIT编译器编译IL代码为native代码, 编译器笔非托管的编译器对执行环境知道的更多. 这里是托管的代码优于非托管的代码的几个地方:

Ÿ   JIT编译器能确定应用程序是否运行在Intel奔腾4 CPU, 然后产生native代码能够充分利用奔腾4提供的任何特别的指令. 通常, 非托管的应用程序被编译为面向具有最小通用功能集合的CPU平台, 一般会避免使用特殊的指令, 这些指令可能会带来性能的提升.

Ÿ   JIT编译器能确定某个测试在它所运行的机器上总是false, 例如, 考虑包含如下代码的函数:

if (numberOfCPUs > 1) {

 

}

这段代码会使JIT编译器不产生任何CPU指令, 如果宿主机器只有一个CPU, 在这种情况下, native代码将会为宿主机器作出很好的调整, 产生代码很小, 执行的更快.

Ÿ   当应用能程序运行时, CLR能分析代码的执行情况, 然后重新将IL编译为native代码, 这种重新编译的代码会重新组织, 根据观察到的执行模式, 减少不正确的分支预测.

这些是你期望托管的代码比非托管代码执行的更快的一些原因, 正像我所说的, 当前的大多数应用程序的性能都非常好, 而且随着时间的进展, 它们的性能还会更好.

如果你的试验表明CLRJIT编译器没有提供给你的应用程序所需要的性能, 你可能想利用NGen.exe工具, 这个工具是随着.NET Framework SDK一起发布的. 该工具编译一个程序集的所有的IL代码为native代码, 并将产生的native代码保存到磁盘上的文件. 在运行时, 当一个程序集被载入时, CLR会自动检查程序集的预编译版本是否存在, 如果存在, 那么CLR载入预编译版本, 因此在运行时不再需要编译. 注意NGen.exe对实际执行的环境所做的假设很保守, 因为这个原因, NGen.exe产生的代码不能像JIT编译器产生的代码那样高度地优化. 我将在后面详细讨论NGen.exe工具.

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值