c# - 為什麼添加局部變量會使.NET代碼變慢



performance compiler-construction jit (5)

這是.NET Framework中的一個錯誤。

好吧,我真的只是猜測,但我提交了一份關於Microsoft Connect的錯誤報告,看看他們說了什麼。 在Microsoft刪除該報告後,我在GitHub上的roslyn項目中重新提交了該報告。

更新: Microsoft已將此問題移至coreclr項目。 從關於這個問題的評論來看,把它稱為bug似乎有點強烈; 它更像是一個缺失的優化。

為什麼要註釋這個for循環的前兩行,並在42%的加速時取消註釋第三個結果?

int count = 0;
for (uint i = 0; i < 1000000000; ++i) {
    var isMultipleOf16 = i % 16 == 0;
    count += isMultipleOf16 ? 1 : 0;
    //count += i % 16 == 0 ? 1 : 0;
}

在時序背後是非常不同的彙編代碼:循環中的13對7指令。 該平台是運行.NET 4.0 x64的Windows 7。 啟用了代碼優化,測試應用程序在VS2010之外運行。 [ 更新: Repro項目 ,對驗證項目設置很有用。]

消除中間佈爾值是一個基本的優化,是我1980年代龍書中最簡單的一個。 在生成CIL或JITing x64機器代碼時,如何不應用優化?

有沒有“真正的編譯器,我希望你優化這段代碼,請”切換? 雖然我同情過早優化類似於對金錢熱愛的情緒,但我可以看到試圖描述一個複雜算法的挫敗感,這個算法在整個慣例中分散。 你可以通過熱點工作,但沒有暗示更廣泛的溫暖區域可以通過手動調整我們通常認為理所當然的編譯器來大大改善。 我當然希望我在這裡遺漏一些東西。

更新: x86也會出現速度差異,但取決於方法即時編譯的順序。 請參閱為什麼JIT訂單會影響性能?

彙編代碼 (根據要求):

    var isMultipleOf16 = i % 16 == 0;
00000037  mov         eax,edx 
00000039  and         eax,0Fh 
0000003c  xor         ecx,ecx 
0000003e  test        eax,eax 
00000040  sete        cl 
    count += isMultipleOf16 ? 1 : 0;
00000043  movzx       eax,cl 
00000046  test        eax,eax 
00000048  jne         0000000000000050 
0000004a  xor         eax,eax 
0000004c  jmp         0000000000000055 
0000004e  xchg        ax,ax 
00000050  mov         eax,1 
00000055  lea         r8d,[rbx+rax] 
    count += i % 16 == 0 ? 1 : 0;
00000037  mov         eax,ecx 
00000039  and         eax,0Fh 
0000003c  je          0000000000000042 
0000003e  xor         eax,eax 
00000040  jmp         0000000000000047 
00000042  mov         eax,1 
00000047  lea         edx,[rbx+rax] 

我傾向於這樣想:在編譯器上工作的人每年只能做這麼多東西。 如果在那個時候他們可以實現lambdas或許多經典的優化,我會投票給lambdas。 C#是一種在代碼讀取和寫入工作方面非常有效的語言,而不是在執行時間方面。

因此,團隊專注於最大化讀/寫效率的功能,而不是某個極端情況下的執行效率(其中可能有數千個)是合理的。

最初,我相信,這個想法是JITter會做所有的優化。 不幸的是,JITting需要花費大量時間,任何高級優化都會使情況變得更糟。 所以這並沒有像人們希望的那樣好。

我發現在C#中編寫真正快速代碼的一件事是,在你提到的任何優化之前,你經常遇到嚴重的GC瓶頸會產生影響。 就像你分配數百萬個對像一樣。 C#在避免成本方面給你留下的很少:你可以使用結構數組,但結果代碼相比之下真的很難看。 我的觀點是,關於C#和.NET的許多其他決策使得這樣的特定優化不如它們在C ++編譯器中那樣值得。 哎呀,他們甚至放棄了NGEN中針對CPU的優化,為程序員(調試器)的效率提供了交易性能。

說完這一切之後,我會喜歡 C#,它實際上利用了自20世紀90年代以來C ++使用的優化。 只是不要犧牲像async / await這樣的功能。


我不能說.NET編譯器或它的優化,甚至不能說它執行它的優化。

但在這種特定情況下,如果編譯器將該布爾變量折疊到實際語句中,並且您嘗試調試此代碼,則優化代碼將與編寫的代碼不匹配。 您將無法單步執行isMulitpleOf16分配並檢查其值。

這只是可以關閉優化的一個例子。 可能還有其他人。 優化可以在代碼的加載階段期間發生,而不是從CLR的代碼生成階段發生。

現代運行時非常複雜,特別是如果您在運行時投入JIT和動態優化。 感謝代碼完成它所說的話。


我認為這與你的其他問題有關。 當我按如下方式更改您的代碼時,多行版本獲勝。

哎呀,只在x86上。 在x64上,多行是最慢的,並且條件性地勝過它們。

class Program
{
    static void Main()
    {
        ConditionalTest();
        SingleLineTest();
        MultiLineTest();
        ConditionalTest();
        SingleLineTest();
        MultiLineTest();
        ConditionalTest();
        SingleLineTest();
        MultiLineTest();
    }

    public static void ConditionalTest()
    {
        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();
        int count = 0;
        for (uint i = 0; i < 1000000000; ++i) {
            if (i % 16 == 0) ++count;
        }
        stopwatch.Stop();
        Console.WriteLine("Conditional test --> Count: {0}, Time: {1}", count, stopwatch.ElapsedMilliseconds);
    }

    public static void SingleLineTest()
    {
        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();
        int count = 0;
        for (uint i = 0; i < 1000000000; ++i) {
            count += i % 16 == 0 ? 1 : 0;
        }
        stopwatch.Stop();
        Console.WriteLine("Single-line test --> Count: {0}, Time: {1}", count, stopwatch.ElapsedMilliseconds);
    }

    public static void MultiLineTest()
    {
        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();
        int count = 0;
        for (uint i = 0; i < 1000000000; ++i) {
            var isMultipleOf16 = i % 16 == 0;
            count += isMultipleOf16 ? 1 : 0;
        }
        stopwatch.Stop();
        Console.WriteLine("Multi-line test  --> Count: {0}, Time: {1}", count, stopwatch.ElapsedMilliseconds);
    }
}

這是由於非規範化的浮點使用。 如何擺脫它和性能損失? 在搜索互聯網尋找殺死非正規數字的方法之後,似乎還沒有“最好”的方式來做到這一點。 我發現了這三種方法可能在不同的環境下效果最好:

  • 在某些GCC環境中可能不起作用:

    // Requires #include <fenv.h>
    fesetenv(FE_DFL_DISABLE_SSE_DENORMS_ENV);
    
  • 在某些Visual Studio環境中可能不起作用: 1

    // Requires #include <xmmintrin.h>
    _mm_setcsr( _mm_getcsr() | (1<<15) | (1<<6) );
    // Does both FTZ and DAZ bits. You can also use just hex value 0x8040 to do both.
    // You might also want to use the underflow mask (1<<11)
    
  • 看起來可以在GCC和Visual Studio中工作:

    // Requires #include <xmmintrin.h>
    // Requires #include <pmmintrin.h>
    _MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);
    _MM_SET_DENORMALS_ZERO_MODE(_MM_DENORMALS_ZERO_ON);
    
  • 英特爾編譯器可以選擇在現代英特爾CPU上默認禁用非正常模式。 更多細節在這裡

  • 編譯器開關。 -ffast-math-msse-mfpmath=sse將會禁用非-mfpmath=sse-mfpmath=sse並使其他一些事情更快,但不幸的是還會做很多其他的可能會破壞您的代碼的近似值。 仔細測試! Visual Studio編譯器的快速數學等價物是/fp:fast但我無法確認這是否也會禁用非規範化。 1





c# .net performance compiler-construction jit