《CLR via C#》第四版
为什么有时候有JIT的语言会比直接编译为机器码的语言快?
简而言之,就是JIT所知道的信息比那些在开发机上直接编译为机器码的的编译器知道的信息更多,有的时候这些信息是如此的有用,以至于效果可以超过JIT本身的开销和JIT编译时间受限带来的限制。《CLR》给出了如下三种具体的原因:
1、 JIT知道当前正在使用的CPU的特性,可以使用那些在当前CPU上具备的新指令集。而传统编译器只能按照开发人员的指示,限制为仅使用较通用的指令集。更重要的是,这个过程是自动的,对开发人员透明的。
2、 在执行环境中,程序中有些变量可能已经变为常量,如当前CPU核心数、当前进程是否为64位进程、进程启动时的命令行参数,这样程序中依赖这些变量的分支/逻辑就可以被优化。给JIT进一步优化最终代码提供了操作空间。
3、 JIT并未只生成代码一次,在程序运行过程中,还可以监视那些部分是程序的热点,即被大量反复执行的代码,对这些代码可以根据profile的信息重新生成更为优化的代码。
除此以外,我认为还有一个重要的原因是:
JIT可以跨越程序集文件的边界来进行优化、内联,这些程序集在编译生成的时候可能是在不同的时间、不同的机器上,传统编译器对此无能为力。
checked 关键字的作用范围仅在当前所在函数内,不影响checked块中调用的函数,所以下面这段代码不会抛异常
class Program { static void Main(string[] args) { checked { Test(); } } private static void Test() { byte a = 100; a += 200; } }
其实想想也正常,checked关键字的作用是将数值运算编译为带检查的IL指令,如果调用的函数在另一个程序集中,该程序集早已被编译好,又如何改变呢?
但注意:Decimal并非基本类型,四则运算没有IL指令对应,所以不受checked影响,其运算始终会抛异常。
值类型表示不会额外为此对象在对上分配,而值类型自己可能被包含在一个引用类型中,所以值类型未必不会在堆上。
值类型也用new关键字,容易给人造成误解。
值类型可以通过显示指定将多个值类型的字段重叠在一起。
只有C++/CLI才能获得指向已装箱的值类型的指针,C#只能先拆箱。
当作为模板的类型参数时,值类型会强制CLR为它专门生成一份特化的代码,而只有引用类型的模板实例可以共享代码,减少内存占用。
个人经验:
引用类型new一次只有一个实例,而值类型则未必,当值类型被传递和修改,其行为需要仔细分析各个值类型变量的生存期,给开发人员带来不小的负担,这里面包括所谓的装箱拆箱。建议只在局部范围内或是作为只读对象的情况下才考虑使用,因为C#码农普遍没有C++码农那样对对象生存期有明确的把握的能力,容易被猪队友害死。
在GC回收时,某个对象即使没超出C++意义上的生存范围(所在的块),但由于在下面未运行的代码中没有被引用所以一样会被认为没有被引用而被GC。在/debug模式下,对象生存期会延长到函数体结束。
using System; using System.Threading; public static class Program { public static void Main() { // Create a Timer object that knows to call our TimerCallback // method once every 2000 milliseconds. Timer t = new Timer(TimerCallback, null, 0, 2000); // Wait for the user to hit <Enter>. Console.ReadLine(); } private static void TimerCallback(Object o) { // Display the date/time when this method got called. Console.WriteLine("In TimerCallback: " + DateTime.Now); //Only once! // Force a garbage collection to occur for this demo. GC.Collect(); } }
在进程正常结束的时候CLR也会执行GC过程,并释放对象。
对于非托管的资源,建议使用SafeHandle系列管理其句柄,其基类CriticalFinalizerObject有如下CLR级别支持的额外特性:
1、 从CriticalFinalizerObject继承的类型首次被引用时,会JIT其析构函数,确保其在析构时不会因内存不足而失败。
2、 在析构时,会优先析构其他不是从CriticalFinalizerObject继承的对象,使得在普通类型的析构函数中可以使用CriticalFinalizerObject类型的对象。
3、 当整个AppDomain被强行卸载时,CriticalFinalizerObject对象的析构函数仍然会被调用。
此外:
1、 SafeHandle对象可以在P/Invoke时替代IntPtr作为参数和返回类型,确保异常安全。
2、 P/Invoke时会正确管理内部的引用计数,确保多线程引用的情况下不会被提前意外释放。
3、 CriticalHandle是不带引用计数的SafeHandle。
4、 SafeHandle和CriticalHandle及其子类都是抽象类,在具体场景需要通过继承的方式使用。
dynamic类型被处理为Object+DynamicAttribute,所以不能通过Object和dynamic来实现不同的重载。
const的值会在编译时被内联,readonly则不会,所以未来可能需要改动的值不应该用const。
Nullable类型在装箱时CLR会特殊处理,脱掉Nullable,即null被装箱为null,v被装箱为v,而不是Nullable<V>类型。
可以通过AppDomain的FirstChanceException事件监视异常被抛出,但事件回调函数不能处理这个异常。
如果一个异常没有被CLR处理,被报告至Windows Error Reporting,那么它获得的调用栈只能到最近一次被throw或是re-throw的位置,即re-throw对Windows Error Reporting无效,仍然会重置异常抛出点。
在Catch和Finally块中,线程不会被Abort所中断.
Environment.FailFast可以跳过普通的异常处理逻辑和对象Finalize方法直接结束进程。
使用反射调用时,如果抛出异常,会将该异常包裹为TargetInvocationException;dynamic不受此影响。
Constrained Execution Regions (CERs),该功能可以让CLR预先在try块之前“准备”一段代码,而不是在运行过程中由于载入DLL失败、类静态初始化失败等原因抛出异常。
Thread.Sleep(0)可以将CPU让给同优先级或更高优先级的线程,而Thread.Yield可以将CPU让给更低优先级的,介于Thread.Sleep(0)和Thread.Sleep(1)之间。