
如果问题是C#怎么才能和C一样快那么真正的问题就是C#到底是慢在哪。内联是诸多影响C#性能中的一个如果频繁调用的大量小函数没有内联那么对性能的影响是非常大的因为建栈、删栈、压栈和跳转的时间加起来很可能比实际执行函数体的时间还长。在实际的应用中Milo Yip的《C/C# /F#/Java/JS/Lua/Python/Ruby渲染比试》是非常好的例子典型的计算密集的应用里面有大量向量计算的小函数调用。结果C#的表现令人失望性能落后VC版本一倍还多即使我改成struct out ref的形式(代码请参见Milo文章)虽然性能略有提高但是差距仍然较大。首先想到是否因为.NET CLR没有内联这些小函数导致的这个性能差异呢。实践出真知赶快调试看看不知道如何看JIT生成的ASM的同学可以看Clayman的这篇文章。结果是我猜错了.NET的JIT编译器已经内联了这些函数。如下面向量按分量乘法的调用处Vec.mul(out rad, ref f, ref rad);0000067e fld qword ptr [ebp-78h]00000681 fmul qword ptr [ebpFFFFFF58h]00000687 fstp qword ptr [ebpFFFFFF58h]0000068d fld qword ptr [ebp-70h]00000690 fmul qword ptr [ebpFFFFFF60h]00000696 fstp qword ptr [ebpFFFFFF60h]0000069c fld qword ptr [ebp-68h]0000069f fmul qword ptr [ebpFFFFFF68h]000006a5 fstp qword ptr [ebpFFFFFF68h]看来并不是因为没有内联而造成的性能差异不禁要深入思考下内联的问题一定不是所有的函数都会内联的那么究竟.NET JIT内联的规则是什么呢。一定有比掷骰子更高明点的办法。Google找到了一篇关于.NET CLR的内联问题好文章《Inline or not to Inline: That is the question》 博主Vance Morrison号称是.NET Runtime的架构师并且主要关注.NET Runtime的性能问题。听起来很牛哦。以下是他的主要观点内联并不总是好的内联的确会减少总的运行指令数。但是另一方面会增大代码尺寸这在代码量比较大的时候可能会降低指令cache的命中率如果L1 cache miss了需要从L2读指令的情况会浪费3-10个时钟周期而如果L2也Miss了需要从内存读的话浪费的更多。而且更大的代码尺寸会降低程序启动的速度。.NET JIT取消了对于多大函数可以内联的硬性规则.NET项目组对应何种情况应该内联做了大量实验JIT在决定是否进行inline是没有足够的信息得知整个程序的运行流程所以结果不会总是对的但以下是显而易见的1.如果内联减小了代码的大小那么一定会内联。注意我们说的尺寸是指本机代码(Native)的尺寸而不是IL代码的尺寸。2.调用越频繁的函数越可能被内联从而得到更好的性能比如在循环内的调用比循环外的内联的机会更大。3.内联可能带来更好的优化的情况更可能被内联比如值类型参数的函数更可能被内联因为内联值类型参数的函数通常可以带来更好的优化效果。JIT采用如下启发式算法来进行判断1.评估非内联情况下的调用体大小。2.评估在内联情况下的调用体大小这个评估是基于IL的我们用一个简单的状态机Markov Model猜测是隐式马尔科夫模型其中使用的评估逻辑基于大量的实测数据。3.计算一个系数。默认是1.4.如果代码在循环里增加系数。5x5.(原文Increase the multiplier if it looks like struct optimizations will kick in). 没太明白是结构性的优化还是指值类型中的struct。6.如果 内联的大小 不内联的大小 * 系数 则进行内联结论很简单1.内联对C#来说是透明的JIT会搞定的要相信组织。2.小的函数更容易被内联。因为内联后不会显著增大代码尺寸。3.在循环体内的函数调用更容易被内联。4.使用值类型参数的函数更容易被内联。对于上面的观点我进行了验证结果如下1.的确实际情况中同一个函数在循环内一般会内联而外面不会。如同样的向量normal()函数。1234567891011publicstaticvoidmul(outVec result,refVec a,refVec b){result.x a.x * b.x;result.y a.y * b.y;result.z a.z * b.z;}publicvoidnormal(){mul(outthis,refthis, 1 / Math.Sqrt(x * x y * y z * z));}A情况没有内联调用在主函数开头即整个程序只会运行一次rd.normal();0000007d lea ecx,[ebp-40h]00000080 call dword ptr ds:[00143978h]B情况内联了调用在radiance函数中而radiance在主函数的多次循环内u.normal();000003e9 fld qword ptr [ebpFFFFFF28h]000003ef fmul st,st(0)000003f1 fld qword ptr [ebpFFFFFF30h]000003f7 fmul st,st(0)000003f9 faddp st(1),st000003fb fld qword ptr [ebpFFFFFF38h]00000401 fmul st,st(0)00000403 faddp st(1),st00000405 fsqrt00000407 fld100000409 fdivrp st(1),st0000040b fld st(0)0000040d fmul qword ptr [ebpFFFFFF28h]00000413 fstp qword ptr [ebpFFFFFF28h]00000419 fld st(0)0000041b fmul qword ptr [ebpFFFFFF30h]00000421 fstp qword ptr [ebpFFFFFF30h]00000427 fmul qword ptr [ebpFFFFFF38h]0000042d fstp qword ptr [ebpFFFFFF38h]可见的确在循环体内的函数更可能被inline而且normal函数是比较大的。所以是否内联得看调用情况直接调用一个函数看是否内联是不行的。2.我测试了.NET 4 CP和.NET 3.5 2.0的情况发现JIT内联生成的代码是不一样的。如上面的mul函数的同一处调用为例在 .NET 2.0、3.0、3.5下生成的代码