JIT & loop optimization
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | using System; namespace ConsoleApplication1 { class TestMath { static void Main() { double res = 0.0; for(int i =0;i<1000000;++i) res += System.Math.Sqrt(2.0); Console.WriteLine(res); Console.ReadKey(); } } } |
通过在C++版本上对该代码进行基准测试,我发现性能比C++版本慢10倍。我对这一点没有异议,但这就引出了以下问题:
似乎(经过几次搜索)JIT编译器不能像C++编译器那样优化这个代码,也就是说只调用一次SqRT,并在其中加上** 1000000。
有没有办法强迫JIT去做?
我重申,我在1.2毫秒时钟的C++版本,C版本12.2毫秒。如果您查看机器代码,C++代码生成器和优化器发出的原因很容易看出。它像这样重写循环(使用c等价物):
1 2 3 4 | double temp = Math.Sqrt(2.0); for (int i = 0; i < 1000000; ++i) { res += temp; } |
这是两种优化的组合,称为"不变代码运动"和"循环提升"。换句话说,C++编译器对Sqt()函数了解得足够多,知道它的返回值不受周围代码的影响,因此可以随意移动。然后将代码移出循环并创建一个额外的局部变量来存储结果是值得的。计算sqrt()比添加要慢。听起来很明显,但这是一个规则,必须内置到优化器中,并且必须考虑,许多规则中的一个。
是的,抖动优化器错过了这个。它不能够花费与C++优化器相同的时间,它在很长的时间约束下运行。因为如果时间太长,那么程序启动就需要太多时间。
开玩笑:一个C程序员需要比代码生成器更聪明一点,并且自己认识到这些优化机会。这是一个相当明显的问题。好吧,现在你知道了。)
要进行所需的优化,编译器必须确保函数
编译器可以做各种各样的检查,检查函数是否没有使用任何其他"外部"变量来查看它是否是无状态的。但这并不总是意味着它不会受到副作用的影响。
在循环中调用函数时,应该在每次迭代中调用它(想想多线程环境,看看为什么这很重要)。所以,通常情况下,如果用户想要这样的优化,就得把常量从循环中去掉。
回到C++编译器——编译器可能对库函数有一定的优化。许多编译器试图优化重要的库,比如数学库,这样可能是特定于编译器的。
另一个很大的区别是C++中通常包含头文件中的一些东西。这意味着编译器可能拥有决定函数调用在调用之间是否不变所需的所有信息。
.NET编译器(在编译时为Visual Studio)并不总是具有所有要分析的代码。大多数库函数已经编译(到IL-第一阶段)。因此,考虑到第三方DLL,可能无法进行深度优化。而且在JIT(运行时)编译时,跨程序集进行这种优化可能会花费太大的成本。
如果
此外,这样的循环可以合理地转换为代码:
1 | double res = 1000000 * Math.Sqrt(2.0); |
理论上,编译器或JIT可以自动执行此操作。然而,我怀疑它会优化实际代码中很少发生的模式。
我打开了resharper的一个特性请求,建议设计时工具建议进行这样的重构。