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这么做?
发布于 2012-12-25 06:02:55
我再现了一下,C++版本的时钟是1.2毫秒,C#版本的时钟是12.2毫秒。如果您查看一下C++代码生成器和优化器发出的机器代码,就很容易看出原因。它像这样重写循环(使用C#等效项):
double temp = Math.Sqrt(2.0);
for (int i = 0; i < 1000000; ++i) {
res += temp;
}
这是两个优化的组合,称为“不变代码运动”和“循环提升”。换句话说,C++编译器对sqrt()函数有足够的了解,知道它的返回值不受周围代码的影响,所以可以随意移动。因此,将代码移到循环之外并创建一个额外的局部变量来存储结果是值得的。而且计算sqrt()比加法慢。这听起来很明显,但这是一个必须内置到优化器中的规则,并且必须加以考虑,这是许多规则中的一个。
是的,抖动优化器遗漏了这一点。它不能花费与C++优化器相同的时间,这是有罪的,它在严重的时间限制下运行。因为如果它花费的时间太长,那么程序启动的时间就太长了。
开玩笑: C#程序员需要比代码生成器聪明一点,并认识到这些优化机会。这是一个相当明显的问题。好了,现在你已经知道它了:)
发布于 2012-12-25 04:17:07
要进行您想要的优化,编译器必须确保函数Sqrt()
对于某个输入总是返回相同的值。
编译器可以执行所有类型的检查,检查函数是否没有使用任何其他“外部”变量,以查看它是否是无状态的。但这并不总是意味着它不会受到副作用的影响。
当一个函数在循环中被调用时,它应该在每次迭代中被调用(想一想多线程环境为什么这很重要)。因此,通常情况下,如果用户想要这种优化,就需要从循环中去掉不变的东西。
回到C++编译器-编译器可能对其库函数进行了某些优化。许多编译器试图优化重要的库,比如数学库,所以这可能是编译器特有的。
另一个很大的不同之处在于,在C++中,通常会包含来自头文件的内容。这意味着如果函数调用在两次调用之间没有变化,编译器可能拥有它所需的所有信息。
.Net编译器(在编译时- Visual Studio)并不总是有要解析的所有代码。大多数库函数已经编译(进入IL - first阶段)。因此,考虑到第三方dlls,可能无法进行深度优化。而且在JIT (运行时)编译时,跨程序集进行这些类型的优化可能成本太高。
发布于 2012-12-25 05:41:46
如果Math.Sqrt
被注释为[Pure]
,它可能会对JIT (甚至C#编译器)有所帮助。然后,假设函数的参数是常量,就像您的示例中的参数一样,可以将该值的计算提升到循环之外。
更重要的是,这样的循环可以合理地转换为代码:
double res = 1000000 * Math.Sqrt(2.0);
理论上,编译器或JIT可以自动执行此操作。然而,我怀疑它是针对一个在实际代码中很少出现的模式进行优化的。
我打开了一个feature request for ReSharper,建议设计时工具建议这样的重构。
https://stackoverflow.com/questions/14025300
复制相似问题