编译器是否优化对const变量和文字const数的操作?

时间:2015-06-08 15:20:07

标签: c# .net optimization

我们说我有课堂上课:

const double magicalConstant = 43;

这是代码中的某个地方:

double random = GetRandom();
double unicornAge = random * magicalConstant * 2.0;

编译器会优化我的代码,以便每次计算magicalConstant * 2.0时都不会计算unicornAge吗?

我知道我可以定义下一个考虑这种乘法的const。但是在我的代码中看起来更清晰。编译器优化它是有意义的。

4 个答案:

答案 0 :(得分:14)

(这个问题是the subject of my blog in October 2015;感谢有趣的问题!)

你已经有一些很好的答案可以回答你的事实问题:不,C#编译器不会生成代码来执行单次乘法86.它生成乘法乘以43并乘以2。

这里有一些细微之处,但没有人进入过。

乘法是"左关联"在C#中。也就是说,

x * y * z

必须计算为

(x * y) * z

而不是

x * (y * z)

现在,您是否曾为这两项计算获得不同的答案?如果答案是"否"然后该操作被称为"关联操作" - 也就是说,我们把括号放在哪里并不重要,因此可以进行优化以将括号放在最佳位置。 (注意:我在之前编辑的这个答案中犯了错误,我说过#34;交换"当我的意思是#34;关联" - 交换操作是x * y等于y * x。)

在C#中,字符串连接是一种关联操作。如果你说

myString + "hello" + "world" + myString

然后你得到的结果与

相同
((myString + "hello") + "world") + myString

(myString + ("hello" + "world")) + myString

因此C#编译器可以在这里进行优化;它可以在编译时进行计算并生成代码,就像编写了

一样
(myString + "helloworld") + myString

实际上是C#编译器的功能。 (有趣的是:实现优化是我加入编译团队时首先要做的事情之一。)

乘法可以进行类似的优化吗? 仅当乘法是关联的时。但事实并非如此!有几种方法不是。

让我们看一个稍微不同的案例。假设我们有

x * 0.5 * 6.0

我们可以这么说吗

(x * 0.5) * 6.0

相同
x * (0.5 * 6.0)

并生成乘法3.0?不。假设x 如此小,x乘以0.5将四舍五入为。然后零次6.0仍为零。因此第一种形式可以给出零,第二种形式可以给出非零值。由于这两个操作给出了不同的结果,因此操作不是关联的。

C#编译器可以添加智能 - 就像我为字符串连接做的那样 - 弄清楚在哪种情况下乘法关联并进行优化,但坦率地说它根本就不值得它。保存字符串连接是一个巨大的胜利。字符串操作的时间和内存都很昂贵。程序包含很多字符串连接,其中常量和变量混合在一起非常常见。浮点运算在时间和内存上非常便宜,很难知道哪些是关联的,并且在现实程序中很少有长链乘法。设计,实施和测试优化所需的时间和精力将更好地用于编写其他功能。

答案 1 :(得分:5)

在你的特定情况下,它不会。 我们考虑以下代码:

class Program
{
    const double test = 5.5;

    static void Main(string[] args)
    {
        double i = Double.Parse(args[0]);
        Console.WriteLine(test * i * 1.5);
    }
}

在这种情况下,常数不会折叠:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       36 (0x24)
  .maxstack  2
  .locals init ([0] float64 i)
  IL_0000:  ldarg.0
  IL_0001:  ldc.i4.0
  IL_0002:  ldelem.ref
  IL_0003:  call       float64 [mscorlib]System.Double::Parse(string)
  IL_0008:  stloc.0
  IL_0009:  ldc.r8     5.5
  IL_0012:  ldloc.0
  IL_0013:  mul
  IL_0014:  ldc.r8     1.5
  IL_001d:  mul
  IL_001e:  call       void [mscorlib]System.Console::WriteLine(float64)
  IL_0023:  ret
} // end of method Program::Main

但总的来说它会得到优化。此优化称为constant folding

我们可以证明这一点。这是C#中的测试代码:

class Program
{
    const double test = 5.5;

    static void Main(string[] args)
    {
        Console.WriteLine(test * 1.5);
    }
}

以下是来自ILDasm的反编译代码:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       15 (0xf)
  .maxstack  8
  IL_0000:  ldc.r8     8.25
  IL_0009:  call       void [mscorlib]System.Console::WriteLine(float64)
  IL_000e:  ret
} // end of method Program::Main

如您所见,IL_0000: ldc.r8 8.25编译器已计算出表达式。

有些人说这是因为你正在处理浮动,但事实并非如此。即使在整数上也不会发生优化:

class Program
{
    const int test = 5;

    static void Main(string[] args)
    {
        int i = Int32.Parse(args[0]);
        Console.WriteLine(test * i * 2);
    }
}

Il Code(无折叠):

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       20 (0x14)
  .maxstack  2
  .locals init ([0] int32 i)
  IL_0000:  ldarg.0
  IL_0001:  ldc.i4.0
  IL_0002:  ldelem.ref
  IL_0003:  call       int32 [mscorlib]System.Int32::Parse(string)
  IL_0008:  stloc.0
  IL_0009:  ldc.i4.5
  IL_000a:  ldloc.0
  IL_000b:  mul
  IL_000c:  ldc.i4.2
  IL_000d:  mul
  IL_000e:  call       void [mscorlib]System.Console::WriteLine(int32)
  IL_0013:  ret
} // end of method Program::Main

答案 2 :(得分:4)

不,在这种情况下不会。

看看这段代码:

const double magicalConstant = 43;
static void Main(string[] args)
{
    double random = GetRandom();
    double unicornAge = random * magicalConstant * 2.0;
    Console.WriteLine(unicornAge);
}

[MethodImpl(MethodImplOptions.NoInlining)]
private static double GetRandom()
{
    return new Random().NextDouble();
}

我们的反汇编是:

        double random = GetRandom();
00007FFDCD203C92  in          al,dx  
00007FFDCD203C93  sub         al,ch  
00007FFDCD203C95  mov         r14,gs  
00007FFDCD203C98  push        rdx  
        double unicornAge = random * magicalConstant * 2.0;
00007FFDCD203C9A  movups      xmm1,xmmword ptr [7FFDCD203CC0h]  
00007FFDCD203CA1  mulsd       xmm1,xmm0  
00007FFDCD203CA5  mulsd       xmm1,mmword ptr [7FFDCD203CC8h]  
        Console.WriteLine(unicornAge);
00007FFDCD203CAD  movapd      xmm0,xmm1  
00007FFDCD203CB1  call        00007FFE2BEDAFE0  
00007FFDCD203CB6  nop  
00007FFDCD203CB7  add         rsp,28h  
00007FFDCD203CBB  ret  

我们这里有两条mulsd指令,因此我们有两次乘法运算。

现在让我们放一些括号:

    double unicornAge = random * (magicalConstant * 2.0);
00007FFDCD213C9A  movups      xmm1,xmmword ptr [7FFDCD213CB8h]  
00007FFDCD213CA1  mulsd       xmm1,xmm0  

如您所见,编译器对其进行了优化。 在浮点数(a*b)*c != a*(b*c)中,因此无需手动帮助即可对其进行优化。

例如,整数代码:

        int random = GetRandom();
00007FFDCD203860  sub         rsp,28h  
00007FFDCD203864  call        00007FFDCD0EC8E8  
        int unicornAge = random * magicalConstant * 2;
00007FFDCD203869  imul        eax,eax,2Bh  
        int unicornAge = random * magicalConstant * 2;
00007FFDCD20386C  add         eax,eax  
带括号的

        int random = GetRandom();
00007FFDCD213BA0  sub         rsp,28h  
00007FFDCD213BA4  call        00007FFDCD0FC8E8  
        int unicornAge = random * (magicalConstant * 2);
00007FFDCD213BA9  imul        eax,eax,56h 

答案 3 :(得分:3)

如果它只是:

double unicornAge = magicalConstant * 2.0;

然后是的,即使编译器不需要执行任何特定的优化,我们也可以合理地期望并假设执行这个简单的优化。正如Eric所指出的那样,这个例子有点误导,因为在这种情况下,编译器也必须将magicalConstant * 2.0视为常量。

但是由于浮点错误(random * 6.0 != (random * 3.0) * 2.0),只有在添加括号时才会替换计算值:

double unicornAge = random * (magicalConstant * 2.0);

编辑:这些浮点错误我在谈论什么? 有两个原因错误:

  • 精确1 :浮点数是近似值,不允许编译器执行任何会改变结果的优化。例如verySmallValue * 0.1 * 10verySmallValue * 0.1更好地曝光(verySmallValue * 0.1) * 10 != verySmallValue * (0.1 * 10)如果0 * 10 == 0将舍入为0(因为fp),那么2^53 - 1因为9007199254740991
  • 精确2 :由于IEEE c * (10 * 0.5)c * 10以上的IEEE 754整数数字无法安全表示,因此您不会仅针对非常小的数字出现此问题如果9007199254740991高于x * 0 >= 0,则a * b * c >= 0可能会给出错误的结果(但稍后会看到官方实施,CPU可能会使用扩展精度)
  • 精确3 :请注意,当b 0a时,c并非始终为真{}} {}}不符合// x = n * c1 * c2 double x = veryHighNumber * 2 * 0.5; veryHighNumber * 2值或关联性。
  • 范围:浮点数值类型具有有限范围,如果第一次乘法将使值为无穷大,则该优化将更改该值。

让我们看一个关于范围问题的例子,因为它比这更微妙。

double

假设x超出+Infinity范围,那么您希望(没有任何优化)veryHighNumber * 2+Infinity(因为+Infinityx == veryHighNumber })。令人惊讶的(?)结果是正确的(或者如果您期望(veryHighNumber * 2) * 0.5)和ldc.r8则不正确(即使编译器保留了您编写的内容并且它为mul生成代码)

为什么会这样?编译器在这里没有执行任何优化,那么CPU必须是有罪的。 C#编译器生成fld qword ptr ds:[00540C48h] ; veryHighNumber fmul qword ptr ds:[002A2790h] ; 2 fmul qword ptr ds:[002A2798h] ; 0.5 fstp qword ptr [ebp-44h] ; x fmul指令,JIT生成它(如果它编译为普通FPU代码,对于生成的SIMD指令,您可以在Eric's answer回答中看到反汇编代码):

ST(0)

ST(0)fmul与内存中的值相乘,并将结果存储在+Infinity中。寄存器处于扩展精度,然后c1链(收缩)不会导致+Infinity直到它不会溢出扩展精度范围(在前面的示例中,也可以使用非常高的数字来检查double x = veryHighNumber * 2 * 0.5; double terriblyHighNumber = veryHighNumber * 2; double x2 = terriblyHighNumber * 0.5; Debug.Assert(!Double.IsInfinity(x)); Debug.Assert(Double.IsInfinity(x2)); Debug.Assert(x != x2); 。)

只有在FPU寄存器中保存中间值时才会发生这种情况,如果您将示例表达式分成多个步骤(其中每个中间值都存储在内存中然后转换回双精度),那么 expect 行为(结果为.box{ width:200px; height:150px; background:grey; float:left; margin:10px; display:block; border:1px solid #999; } .box img{ width:200px; height:150px; border:1px solid #999; } .box span { width:200px; height:30px; background:grey; display:block; text-align:center; font-size:40px; position:absolute; top:140px; line-height:35px; color:#fff; font-size:30px; })。这是IMO更多令人困惑的事情:

<div class="box">
<img src ="http://www.debrecensun.hu/media/2014/01/13-parties-for-wednesday-15-january/party.jpg"/>
<span>TITLE</span>
</div>


<div class="box">
<img src ="http://www.tnpsc.com/downloads2011/StreetRaceCars.jpg"/>
<span>TITLE</span>
</div>