[英]Does compiler optimize operation on const variable and literal const number?
假設我有一個字段:
const double magicalConstant = 43;
這是代碼中的某個地方:
double random = GetRandom();
double unicornAge = random * magicalConstant * 2.0;
將編譯器優化我的代碼,因此它不計算magicalConstant * 2.0
每次calcuates時間unicornAge
?
我知道我可以定義下一個考慮這種乘法的const。 但是在我的代碼中看起來更清晰。 編譯器優化它是有意義的。
(這個問題是2015年10月我博客的主題 ;感謝有趣的問題!)
你已經有了一些很好的答案來回答你的事實問題:不,C#編譯器不會生成代碼來執行86的單次乘法。它生成乘法43乘以2乘以。
這里有一些微妙之處,但沒有人進入過。
乘法在C#中是“左關聯的”。 那是,
x * y * z
必須計算為
(x * y) * z
並不是
x * (y * z)
現在,您是否可以獲得這兩種計算的不同答案? 如果答案是“否”,那么該操作被稱為“關聯操作” - 也就是說,我們放置括號的位置並不重要,因此可以進行優化以將括號放在最佳位置。 (注意:我在之前編輯的這個答案中犯了一個錯誤,當我說“關聯”時我說“交換” - 一個交換操作是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#編譯器可以添加智能 - 就像我為字符串連接做的那樣 - 弄清楚乘法在哪些情況下是關聯的並進行優化,但坦率地說它根本不值得。 保存字符串連接是一個巨大的勝利。 字符串操作的時間和內存都很昂貴。 程序包含很多字符串連接,其中常量和變量混合在一起非常常見。 浮點運算在時間和內存上非常便宜,很難知道哪些是關聯的,並且在現實程序中很少有長鏈乘法。 設計,實現和測試優化所需的時間和精力將更好地用於編寫其他功能。
在你的特定情況下,它不會。 我們考慮以下代碼:
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
但總的來說它會得到優化。 此優化稱為常量折疊 。
我們可以證明這一點。 這是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代碼(無折疊):
.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
不,在這種情況下不會。
看看這段代碼:
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
如果它只是:
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
將舍入為0(因為fp)則(verySmallValue * 0.1) * 10 != verySmallValue * (0.1 * 10)
因為0 * 10 == 0
。 2^53 - 1
( 9007199254740991
)無法安全表示那么c * (10 * 0.5)
可能會給出不正確的結果如果c * 10
是9007199254740991
以上(但是后面會看到,這是官方實現,CPU可能會使用擴展精度)。 x * 0 >= 0
並不總是為真,那么當b
為0
時表達式a * b * c >= 0
0
可以根據a
和c
值或相關性而為真。 讓我們看一個關於范圍問題的例子,因為它比這更微妙。
// x = n * c1 * c2
double x = veryHighNumber * 2 * 0.5;
假設veryHighNumber * 2
超出double
范圍,那么你期望(沒有任何優化) x
是+Infinity
(因為veryHighNumber * 2
是+Infinity
)。 令人驚訝的(?)結果是正確的(或者如果您期望+Infinity
x == veryHighNumber
正確)和x == veryHighNumber
(即使編譯器保留了您編寫的內容並且它為(veryHighNumber * 2) * 0.5
生成代碼)。
為什么會這樣? 編譯器在這里沒有執行任何優化,那么CPU必須是有罪的。 C#編譯器生成ldc.r8
和mul
指令,JIT生成它(如果它編譯為普通的FPU代碼,對於生成的SIMD指令,你可以在Alex的答案中看到反匯編的代碼):
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
將ST(0)
與來自存儲器的值相乘,並將結果存儲在ST(0)
。 寄存器處於擴展精度,然后fmul
鏈( 收縮 )將不會導致+Infinity
直到它不會溢出擴展的精度范圍(在前面的例子中也可以使用非常高的數字來檢查c1
)。
只有當中間值保存在FPU寄存器中時才會發生這種情況,如果您將示例表達式分成多個步驟(其中每個中間值存儲在內存中然后轉換回雙精度),您將獲得預期的行為(結果為+Infinity
)。 這是IMO更令人困惑的事情:
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);
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.