编译器将C#/F#/VB.net源代码和资源文件打包为程序集。程序集内包含托管的模块和资源文件。托管的模块中PE32/PE32+头文件记载了与本机CPU代码有关的信息。如果为纯IL代码的模块,PE32/PE32+通常被忽略。CLR头通常记录了CLR版本、Main方法的信息以及模块的信息和一些数据,我们可以使用CLRVer.exe
命令查询到CLR版本,参数为-all
全部,进程ID
指定进程。
我们可以在Visual Studio设置目标平台信息:
IL代码虽然被称为一种汇编,但是看起来似乎IL比常说的x86汇编简单很多。这种代码将会在程序第一次(而非首次)执行的时候首先读取目标的IL代码然后生成机器码然后修改目标节点的引用指向生成的机器码,当机器码生成结束后程序执行的就是机器码,因为所有的引用都被修改指向了即时生成的机器码。所以托管代码编写的程序仅在第一次执行(而非首次)的时候慢一点,而后就能全速运行。
这种即时编译(JIT)的技术Java目前也在使用,似乎我们看起来使用JIT技术会导致程序性能有点糟糕,但实际上效率可能比自己写的非托管代码的效率还高。我们都知道编译器在编译时期会执行优化操作让程序的运行速度更快,但是编译器编译出来的程序可能在某一具体架构上不同型号的处理器运行,为了确保不出现Intel Pentium、Intel Core i3、Intel Core i5、Intel Core i7、Intel Atom、AMD Opteron等奇葩的版本出现,那么编译器将会以最小处理器指令集编译,这样兼容性确保了,但是新的指令集的优势似乎就没有利用到。
JIT在编译的时候能够知道某台计算机是否具有新的指令集可以使用,一旦可用那么它会使用新的指令集,这样,利用新的指令集就可能带来速度的提升。
同时对于本身不能执行的代码(永假),JIT可以选择不予编译,所以在类似要求AMD处理器的IL代码段在程序运行的时候并不会被编译为机器码。这样亦可提供速度的提升。
所以自己的非托管代码的编写能力没有达到一定程度,托管的代码可能效率更高。
.NET也提供了NGen.exe工具来编译IL代码为机器码并保存为一个文件,首次加载的时候会检查该文件是否符合运行要求的版本,如果符合那么可以直接运行该文件而避免JIT,但是这样可能丧失JIT带来的优势,因为NGen.exe生成的代码不得不考虑最小运行环境。
如果有多处理器,那么可以使用System.Runtime.ProfileOptimization类来优化。它会让CLR检查程序运行时哪些方法被JIT编译,并将结果记录到一个文件。程序再次启动时,就用其他线程并发编译这些方法。这样应用程序运行得更快,因为多个方法并发编译,而且是在应用程序初始化时编译,而不是在用户和程序交互时才“即时”编译。
至于有人为了保护自己的知识产权使用NGen.exe生成的机器代码发布,其实是不能的。纵使NGen.exe生成了一份机器码,但是基于.NET的程序依然会随附一份IL汇编代码以确保在机器码出现问题的时候不会导致程序Crash。
由于.NET利用了JIT,现在的.NET反编译的反编译后的代码质量相当高,所以我们常用的保护知识产权的办法是代码混淆。使用代码混淆器将自己写的程序混淆加密,加大反编译后阅读的难度。有趣的是似乎这些反编译工具(ILDASM.EXE)除外都不能将F#编写的程序文件正确逆向工程。曾尝试使用F#写过一个SqlHelper动态链接库,代码混淆后再使用了三款反编译工具都没能正确逆向出F#代码,即使逆向出来的C#代码也只有函数名而不包含函数体,在整个导出文件夹内的文件中没能够找到函数体。所以如果依然想使用.NET来撰写且不希望透露代码逻辑的,可以尝试F#这门语言。
前面说道NGen.exe产生的文件是不可以发布给用户的,但是针对大型程序我们还是可以利用NGen.exe来优化的,当然这里的大型程序不包括Web应用程序,因为Web应用程序仅在第一次访问的时候需要JIT,而后就不需要JIT了。使用Managed Profile Guided Optimization工具来检查启动的时候需要执行那些东西,然后反馈给NGen.exe优化产生的本机代码镜像,发布前使用MPGO.exe走一边流程后将profile文件整合进程序集,NGen.exe将会利用它来创建更佳的本机镜像。