我们说我有课堂上课:
const double magicalConstant = 43;
这是代码中的某个地方:
double random = GetRandom();
double unicornAge = random * magicalConstant * 2.0;
编译器会优化我的代码,以便每次计算magicalConstant * 2.0
时都不会计算unicornAge
吗?
我知道我可以定义下一个考虑这种乘法的const。但是在我的代码中看起来更清晰。编译器优化它是有意义的。
答案 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);
编辑:这些浮点错误我在谈论什么? 有两个原因错误:
verySmallValue * 0.1 * 10
中verySmallValue * 0.1
更好地曝光(verySmallValue * 0.1) * 10 != verySmallValue * (0.1 * 10)
如果0 * 10 == 0
将舍入为0(因为fp),那么2^53 - 1
因为9007199254740991
。c * (10 * 0.5)
(c * 10
以上的IEEE 754整数数字无法安全表示,因此您不会仅针对非常小的数字出现此问题如果9007199254740991
高于x * 0 >= 0
,则a * b * c >= 0
可能会给出错误的结果(但稍后会看到官方实施,CPU可能会使用扩展精度) b
0
为a
时,c
并非始终为真{}} {}}不符合// x = n * c1 * c2
double x = veryHighNumber * 2 * 0.5;
和veryHighNumber * 2
值或关联性。让我们看一个关于范围问题的例子,因为它比这更微妙。
double
假设x
超出+Infinity
范围,那么您希望(没有任何优化)veryHighNumber * 2
为+Infinity
(因为+Infinity
为x == 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>