16
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 这样做?

4

3 回答 3

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# 程序员需要比代码生成器更聪明一点,并自己认识到这些优化机会。这是一个相当明显的问题。好吧,现在你无论如何都知道了:)

于 2012-12-24T22:02:55.390 回答
6

要进行您想要的优化,编译器必须确保函数Sqrt()将始终为某个输入返回相同的值。

编译器可以做各种检查函数没有使用任何其他“外部”变量来查看它是否是无状态的。但这并不总是意味着它不会受到副作用的影响。

当一个函数在循环中被调用时,它应该在每次迭代中被调用(想想多线程环境,看看为什么这很重要)。因此,如果他想要这种优化,通常由用户决定是否将不断的东西排除在循环之外。

回到 C++ 编译器——编译器可能对其库函数进行了某些优化。许多编译器尝试优化数学库等重要库,因此这可能是特定于编译器的。

另一个很大的不同是在 C++ 中,你通常会从头文件中包含一些东西。这意味着编译器可能拥有决定函数调用在调用之间是否不变所需的所有信息。

.Net 编译器(在编译时 - Visual Studio)并不总是有所有要解析的代码。大多数库函数已经编译(进入 IL - 第一阶段)。因此考虑到第 3 方 dll,可能无法进行深度优化。在 JIT(运行时)编译中,跨程序集进行这种优化可能成本太高。

于 2012-12-24T20:17:07.450 回答
5

如果Math.Sqrt注释为[Pure]. 然后,假设函数的参数在您的示例中是恒定的,则可以将值的计算提升到循环之外。

更重要的是,这样的循环可以合理地转换为代码:

double res = 1000000 * Math.Sqrt(2.0);

理论上,编译器或 JIT 可以自动执行此操作。但是我怀疑它会针对实际代码中很少发生的模式进行优化。

我打开了ReSharper 的功能请求,建议设计时工具建议进行这样的重构。

于 2012-12-24T21:41:45.700 回答