【问题标题】:Would C# compiler or Jitter optimize these kinds of arithmetic operations?C# 编译器或 Jitter 会优化这些算术运算吗?
【发布时间】:2011-12-29 10:12:52
【问题描述】:

假设我有这样的事情:

for (int i = 0; i < 1001; i++)
{
    double step = i / 1000.0;

    // do some math here
}

基本上是转向:

double step = i / 1000.0;

进入这个:

double step = i * 0.001;

我不确定是否可以在不更改程序结果的情况下进行这种更改,但想知道 C# 编译器或抖动是否会这样做?如果不是,为什么?我认为要么不值得,要么他们还没有添加此优化。

【问题讨论】:

  • 第一个必须是 i / 1000.0。否则,除法完全以整数完成,结果始终为零或一。
  • 由于1/1000 不能在 IEEE-754 中精确表示:您基本上是在问:“JIT 会为 fp 数学更快地产生不精确的结果吗?”。这并不是那么荒谬,因为 C++ 编译器长期以来一直包含非严格的数学模式(虽然不知道他们是否会做这样的优化),所以谁知道呢。您必须阅读语言规范才能确定,但​​我认为这不太可能。

标签: c# .net optimization compiler-construction jit


【解决方案1】:

让我们把它分解成几个问题:

抖动可以合法地将d / 1000.0更改为d * 0.001吗?

不,因为这两个计算给出不同的结果。请记住,浮点数是二进制分数,而不是十进制分数; 0.001 作为双精度不完全等于 1 / 1000 不超过 0.333333333 作为双精度完全等于 1 / 3。0.001 是最接近 1/1000 的分数,可以用 52 个二进制位表示。因此存在 x / 1000.0 不等于 x * 0.001 的值。

抖动可以合法地将d / 2.0更改为d * 0.5吗?

是的。在这种情况下,这些值可以精确地用二进制表示,因为 1/2 的底部是 2 的小幂。

抖动还可以将整数除法和乘法(如x / 2x * 2)更改为x &gt;&gt; 1x &lt;&lt; 1

在合法的情况下,抖动真的会这样做吗?

我不知道。试试看!

您要做的是编译“零售”程序,然后启动它不在调试器中并运行它,直到您知道有问题的代码已被 jitted。然后附加调试器并检查 jit 代码。如果知道附加了调试器,抖动将生成更差的代码,因为它正在尝试生成更易于调试的代码。

我认为要么不值得,要么他们还没有添加此优化。

对于除法到乘法的情况,您假设乘法比除法快。现代芯片在这两个方面都非常出色。尽管除法通常确实需要更多位操作,但差异可能可以忽略不计。

【讨论】:

  • 关于可能的性能提升:根据 Aigner 对 SB 的 instruction tablesMULPD 是 1 个周期,而 DIVPD 是 10-22 个周期(倒数吞吐量,所以假设我们可以做足够的 divs/muls ..)
  • @Voo:当然。但是,如果除法一开始就不是瓶颈,那么将其转化为乘法不会对最终用户产生影响。
  • 实际上我想强调您的观点:在 1000 次迭代循环中,每次迭代都不能节省 2 打循环(最好的情况!)?打哈欠,如果发生有趣的事情,请叫醒我(单个缓存未命中会更糟)。此外,JIT 可以进行这种优化的情况也很少——我可以想到 JIT 人员在此之前应该关注的很多事情。但这一切都取决于您的工作:我认为 HPC 人员可能对此有不同的看法。
  • 谢谢埃里克,惊人的回复。顺便说一句,既然你说d/2.0d * 0.5 相同,你知道d/4.0d * 0.25 是否可以说同样的话?
  • @Joan 只需检查 0.25 是否可以精确地以二进制表示(显然它适合单/双 IEEE-754 格式)。在你的情况下:是的,它可以:.01
【解决方案2】:

你可以试试看,但我今天感觉很慷慨,所以我为你做了。

测试一:

    static void Test1(int i)
    {
        double x = i / 1000.0;
        if (x == 0)
            throw new Exception();
    }

(抛出是为了方便在正确的时刻附加调试器)

反汇编(64位):

cvtsi2sd    xmm0,dword ptr [rsp+60h] 
divsd       xmm0,mmword ptr [000000C8h] 

反汇编(32 位):

fild        dword ptr [ebp-4] 
fdiv        dword ptr ds:[0460012Ch] 

好的,测试代码2:i / 2.0
反汇编(64位):

cvtsi2sd    xmm0,dword ptr [rsp+60h] 
divsd       xmm0,mmword ptr [000000C8h] 

反汇编(32 位):

fild        dword ptr [ebp-4] 
fdiv        dword ptr ds:[0460012Ch] 

结论:不,JIT 编译器没有进行这种优化。
有关系吗?不经常。您可以通过编写i * (1 / 1000.0) 或类似的方式轻松“修复”它(在这种情况下必须不断折叠 - 不要删除括号)。

JIT 编译器对整数进行这种优化。

【讨论】:

    【解决方案3】:

    我从这两种方法开始:

    public static double Division(double i)
    {
        return i / 1000.0;
    }
    
    public static double Multiplication(double i)
    {
        return i * 0.001;
    }
    

    编译,然后在ILSpy 中打开程序集。这是生成的 IL:

    .method public hidebysig static 
        float64 Division (
            float64 i
        ) cil managed 
    {
        // Method begins at RVA 0x2052
        // Code size 12 (0xc)
        .maxstack 8
    
        IL_0000: ldarg.0
        IL_0001: ldc.r8 1000
        IL_000a: div
        IL_000b: ret
    } // end of method Program::Division
    
    .method public hidebysig static 
        float64 Multiplication (
            float64 i
        ) cil managed 
    {
        // Method begins at RVA 0x205f
        // Code size 12 (0xc)
        .maxstack 8
    
        IL_0000: ldarg.0
        IL_0001: ldc.r8 0.001
        IL_000a: mul
        IL_000b: ret
    } // end of method Program::Multiplication
    

    如您所见,它不会将乘法更改为除法或将除法更改为乘法。不过,我不清楚一种操作将如何优化另一种操作。

    编辑:忘记了抖动。嗯,这取决于平台。因此,我认为,除非您是 Eric Lippert,否则它甚至无法回答。

    【讨论】:

    • 您在调试配置中构建了它。只是为了记录,为什么不在发布配置中构建它并发布这些结果?
    • 我做到了。我意识到我做了一个调试,然后迅速编辑了我的问题。
    • 嘿,我对抖动一无所知。如果我有这样的问题,我会和其他人一样查看抖动输出的内容。
    • 更多来自 Java pov(但至少在某种程度上也适用于 CLR):IL 没有优化任何东西,这显然是抖动的工作,所以任何时候你问你自己是否 IL 正在优化 XYZ,只要假设否,那么在 98% 的情况下你都是对的。现在 JIT 是否会优化这些东西:看看吧。但是:我严重怀疑它,因为我们在这里处理的 fp 数学是出了名的善变。除以 x 或乘以 1/x 通常不会得到相同的输出,因此我们首先必须检查 1/x 是否可以精确表示。
    【解决方案4】:

    这是你问的吗?我有点不确定..

    for(double i = .001; i < 1.001; i+=.001){
        //TODO: Implement
    }
    

    【讨论】:

    • 谢谢,那也可以。我只是好奇编译器是否能够自己优化这些类型的操作。
    • 你可以采用犬夜叉的方法,除了不是方法,而是让它们重载运算符并使 i 成为自己的类而不是 double 成为 MyDouble,然后重载 / 运算符,而不是将所有内容乘以分母。
    • 这就是你应该如何优化代码。您正在完全删除算术运算。我的“方法”只是证明编译器不会改变代码中的算术,即使它实际上是等价的。
    • 由于 0.001 不能准确地表示为小数,因此这种技术充满了危险。随着舍入误差的累积,小的舍入误差会导致此类代码运行过多或过少。您最好以精确的整数进行循环,并在每次循环中计算浮点数。
    • @EricLippert 我想你的意思是说“因为 0.001 不能准确地表示为双精度”(而不是“...十进制”)。
    猜你喜欢
    • 2012-10-05
    • 2018-08-19
    • 1970-01-01
    • 2014-05-24
    • 2011-10-30
    • 1970-01-01
    • 1970-01-01
    • 2016-09-11
    • 1970-01-01
    相关资源
    最近更新 更多