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去做?
答案 0 :(得分:10)
我重申,我将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#程序员需要比代码生成器更聪明,并且自己认识到这些优化机会。这是一个相当明显的问题。那么,既然你现在已经了解了它:)答案 1 :(得分:6)
要进行所需的优化,编译器必须确保函数 Sqrt()
始终为特定输入返回相同的值。
编译器可以执行各种检查,函数不使用任何其他“外部”变量来查看它是否是无状态的。但这并不总是意味着它不会受到副作用的影响。
当在循环中调用函数时,应该在每次迭代中调用它(想想多线程环境,看看为什么这很重要)。因此,如果他想要进行这种优化,通常由用户自行调整循环。
回到C ++编译器 - 编译器可能对其库函数进行了某些优化。许多编译器尝试优化像数学库这样的重要库,因此可能是特定于编译器的。
另一个很大的区别是在C ++中你通常会从头文件中包含那些东西。这意味着编译器可能拥有决定函数调用在调用之间是否发生变化所需的所有信息。
.Net编译器(在编译时 - Visual Studio)并不总是要解析所有代码。大多数库函数已经编译(进入IL - 第一阶段)。考虑到第三方dll,可能无法进行深度优化。在JIT(运行时)编译中,跨程序集进行这些优化可能成本太高。
答案 2 :(得分:5)
如果Math.Sqrt
注释为[Pure]
,它可能对JIT(甚至C#编译器)有帮助。然后,假设函数的参数在示例中是常量,则可以在循环外提取值的计算。
更重要的是,这样的循环可以合理地转换为代码:
double res = 1000000 * Math.Sqrt(2.0);
理论上,编译器或JIT可以自动执行此操作。但是我怀疑它会优化实际代码中很少发生的模式。
我打开了一个feature request for ReSharper,建议设计时工具建议进行这样的重构。