簡體   English   中英

編譯器是否優化對const變量和文字const數的操作?

[英]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);

編輯 :我正在談論的這些浮點錯誤是什么? 兩個錯誤原因

  • 精度1 :浮點數是近似值,不允許編譯器執行任何會改變結果的優化。 例如(在Eric的答案中更好地暴露)在verySmallValue * 0.1 * 10如果verySmallValue * 0.1將舍入為0(因為fp)則(verySmallValue * 0.1) * 10 != verySmallValue * (0.1 * 10)因為0 * 10 == 0
  • 精度2 :你只有非常小的數字沒有這個問題,因為IEEE 754整數高於2^53 - 19007199254740991 )無法安全表示那么c * (10 * 0.5)可能會給出不正確的結果如果c * 109007199254740991以上(但是后面會看到,這是官方實現,CPU可能會使用擴展精度)。
  • 精度3 :注意x * 0 >= 0並不總是為真,那么當b0時表達式a * b * c >= 0 0可以根據ac值或相關性而為真。
  • 范圍 :浮點數值類型具有有限范圍,如果第一次乘法將使值為無窮大,則優化將更改該值。

讓我們看一個關於范圍問題的例子,因為它比這更微妙。

// 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.r8mul指令,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

fmulST(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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM