渭茱瀑 发表于 2025-5-29 19:36:38

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
00000681  fmul        qword ptr
00000687  fstp        qword ptr
0000068d  fld         qword ptr
00000690  fmul        qword ptr
00000696  fstp        qword ptr
0000069c  fld         qword ptr
0000069f  fmul        qword ptr
000006a5  fstp        qword ptr
 
      看来并不是因为没有内联而造成的性能差异,不禁要深入思考下内联的问题,一定不是所有的函数都会内联的,那么究竟.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]
查看完整版本: Swifter C#之inline还是不inline,这是个问题