【问题标题】:JIT & loop optimizationJIT & 循环优化
【发布时间】:2012-12-24 20:02:42
【问题描述】:
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 去做?

【问题讨论】:

  • 我看不出 JIT 编译器 不能 这样做的任何原因。当前版本可能会也可能不会,但这并不意味着未来版本不会。我很好奇-您是否尝试在发布模式下编译,而不是调试模式?您可能会发现发布模式大大提高了性能。
  • @ChrisShain 你确定编译器可以做到这一点吗?必须确保 Math.Sqrt 没有副作用,因此可以将其减少为单次调用。
  • 这些数字对我来说并没有加起来。 C# 版本运行时间长 10 倍,但调用次数却是一百万次?
  • @John:不,它只是一个教育目的代码。不是生产代码,如果它是“实际”的意思。
  • 除了使用发布版本之外,还要确保您没有附加调试器(即直接在 Visual Studio 之外启动 exe。)

标签: c# optimization jit


【解决方案1】:

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

【讨论】:

  • 为什么编译器需要知道计算 sqrt 比相加慢?此外,C# 编译器不能执行此提升,而不是将其留给 JIT 吗?
  • 好吧,反过来推理。如果 sqrt 比添加更快,那么这将是一个糟糕的优化。 C# 编译器不优化代码,这是一个抖动任务。与 C++ 编译器的想法相同,它是代码生成器的职责。
  • 我一定遗漏了一些东西,因为我看不出如何避免 N 加法操作。改变的是执行一个 sqrt 而不是 N。如果 N==1 那么可能有额外的寄存器变量会更慢,但是如果我们假设 N>1 那么即使被提升的操作本身就是一个加法,我们仍然会出来前面,对吧?
  • 不确定你在说什么。如果您建议将重复的加法转换为乘法(可能在纸上 res = 1000000 * temp),那么,不,这不是有效的优化。由于舍入误差,浮点数学不是关联的,您会得到不同的结果。你当然可以自己这样写。
  • 这不是我想要提出的。我试图绘制的图片涉及将sqrt(2.0) 替换为1.0+2.0(具有固定值的加法运算)。你说sqrt 比加法慢很重要。因此,如果sqrt 的成本与加法相同,那么编译器会做些什么不同的事情呢?我不明白为什么你关于相对成本的说法有什么不同。
【解决方案2】:

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

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

在循环中调用函数时,应该在每次迭代中调用它(想想多线程环境,看看为什么这很重要)。因此,如果用户想要进行这种优化,通常由用户将常量内容从循环中取出。

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

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

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

【讨论】:

    【解决方案3】:

    如果 Math.Sqrt 被注释为 [Pure],它可能有助于 JIT(甚至 C# 编译器)。然后,假设函数的参数在您的示例中是恒定的,则可以将值的计算提升到循环之外。

    而且,这样的循环可以合理地转换成代码:

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

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

    我打开了feature request for ReSharper,提示设计时工具建议进行这样的重构。

    【讨论】:

      猜你喜欢
      • 2020-10-19
      • 1970-01-01
      • 2014-10-09
      • 2018-06-25
      • 2020-04-13
      • 1970-01-01
      • 2018-01-04
      • 1970-01-01
      相关资源
      最近更新 更多