Swifter C#之inline还是不inline,这是个问题

      如果问题是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 [ebp+FFFFFF58h]
00000687  fstp        qword ptr [ebp+FFFFFF58h]
0000068d  fld         qword ptr [ebp-70h]
00000690  fmul        qword ptr [ebp+FFFFFF60h]
00000696  fstp        qword ptr [ebp+FFFFFF60h]
0000069c  fld         qword ptr [ebp-68h]
0000069f  fmul        qword ptr [ebp+FFFFFF68h]
000006a5  fstp        qword ptr [ebp+FFFFFF68h]

 

      看来并不是因为没有内联而造成的性能差异,不禁要深入思考下内联的问题,一定不是所有的函数都会内联的,那么究竟.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.如果代码在循环里增加系数。(5x)

      5.(原文:Increase the multiplier if it looks like struct optimizations will kick in). 没太明白是结构性的优化还是指值类型中的struct。

      6.如果 内联的大小 <= 不内联的大小 * 系数  则进行内联


      结论很简单:


      1.内联对C#来说是透明的JIT会搞定的,要相信组织。

      2.小的函数更容易被内联。因为内联后不会显著增大代码尺寸。

      3.在循环体内的函数调用更容易被内联。

      4.使用值类型参数的函数更容易被内联。

 

      对于上面的观点我进行了验证,结果如下:

 

      1.的确实际情况中同一个函数在循环内一般会内联而外面不会。如同样的向量normal()函数。     

 

public static void mul(out Vec result, ref Vec a, ref Vec b)
{
    result.x = a.x * b.x;
    result.y = a.y * b.y;
    result.z = a.z * b.z;
}

public void normal()
{
    mul(out this, ref this, 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 [ebp+FFFFFF28h]
000003ef  fmul        st,st(0)
000003f1  fld         qword ptr [ebp+FFFFFF30h]
000003f7  fmul        st,st(0)
000003f9  faddp       st(1),st
000003fb  fld         qword ptr [ebp+FFFFFF38h]
00000401  fmul        st,st(0)
00000403  faddp       st(1),st
00000405  fsqrt
00000407  fld1
00000409  fdivrp      st(1),st
0000040b  fld         st(0)
0000040d  fmul        qword ptr [ebp+FFFFFF28h]
00000413  fstp        qword ptr [ebp+FFFFFF28h]
00000419  fld         st(0)
0000041b  fmul        qword ptr [ebp+FFFFFF30h]
00000421  fstp        qword ptr [ebp+FFFFFF30h]
00000427  fmul        qword ptr [ebp+FFFFFF38h]
0000042d  fstp        qword ptr [ebp+FFFFFF38h] 

 

可见的确在循环体内的函数更可能被inline,而且normal函数是比较大的。所以是否内联得看调用情况,直接调用一个函数看是否内联是不行的。

 

2.我测试了.NET 4 CP和.NET 3.5 2.0的情况,发现JIT内联生成的代码是不一样的。如上面的mul函数的同一处调用为例:

在 .NET 2.0、3.0、3.5下生成的代码

                  Vec.mul(out x, ref r.d, t);
000000db  lea         ecx,[esp+10h]
000000df  lea         edx,[ebp+8]
000000e2  cmp         byte ptr [edx],al
000000e4  add         edx,18h
000000e7  mov         eax,edx
000000e9  fld         qword ptr [esp]
000000ec  fstp        qword ptr [esp+000003B0h]
000000f3  fld         qword ptr [eax]
000000f5  fmul        qword ptr [esp+000003B0h]
000000fc  fstp        qword ptr [ecx]
000000fe  fld         qword ptr [eax+8]
00000101  fmul        qword ptr [esp+000003B0h]
00000108  fstp        qword ptr [ecx+8]
0000010b  fld         qword ptr [eax+10h]
0000010e  fmul        qword ptr [esp+000003B0h]
00000115  fstp        qword ptr [ecx+10h]

 

 .NET 4下生成的代码
                  Vec.mul(out x, ref r.d, t);
000000d2  fld         qword ptr [ebp-14h]
000000d5  lea         eax,[ebp+20h]
000000d8  fld         qword ptr [eax]
000000da  fmul        st,st(1)
000000dc  fstp        qword ptr [ebp-30h]
000000df  lea         eax,[ebp+20h]
000000e2  fld         qword ptr [eax+8]
000000e5  fmul        st,st(1)
000000e7  fstp        qword ptr [ebp-28h]
000000ea  lea         eax,[ebp+20h]
000000ed  fld         qword ptr [eax+10h]
000000f0  fmulp       st(1),st
000000f2  fstp        qword ptr [ebp-20h] 

 

      clr 4.0和2.0生成的代码是不同的,而且.NET 4 JIT生成的内联代码效率更高,这也许可以解释为什么这个测试程序在3.5和4里面有较大性能差异,3.5用时86秒,4用时67秒。我仔细查看了测试程序中的调用,在循环内被频繁调用的计算函数都被内联了,只有在循环外只运行一次的没有被内联,看来JIT工作的很好,我们可以放心的把inline的工作交给JIT了。

      既然不是内联导致的性能问题那么造成C#这个测试性能不佳的原因还有什么呢,是因为C#的两次编译无法进行C++那样的更深入全面的优化吗,还是因为其他原因呢?我们还需要继续去探索。

      To be continued. . .

代码
Vec.mul(out rad, ref f, ref rad);
0000067e fld qword ptr [ebp-78h]
00000681 fmul qword ptr [ebp+FFFFFF58h]
00000687 fstp qword ptr [ebp+FFFFFF58h]
0000068d fld qword ptr [ebp-70h]
00000690 fmul qword ptr [ebp+FFFFFF60h]
00000696 fstp qword ptr [ebp+FFFFFF60h]
0000069c fld qword ptr [ebp-68h]
0000069f fmul qword ptr [ebp+FFFFFF68h]
000006a5 fstp qword ptr [ebp+FFFFFF68h]

 

http://www.cnblogs.com/miloyip/archive/2010/07/07/languages_brawl_GI.html

posted on 2010-07-10 23:46 noremorse 阅读(...) 评论(...) 编辑 收藏

导航

公告

统计