簡體   English   中英

C#優化器性能差嗎?

[英]Poor C# optimizer performance?

我剛剛寫了一個小例子檢查,C#的優化器在索引器的情況下表現如何。 這個例子很簡單 - 我只是將一個數組包裝在一個類中並嘗試填充它的值:一次直接和一次索引器(內部訪問數據的方式與直接解決方案完全相同)。

    public class ArrayWrapper
    {
        public ArrayWrapper(int newWidth, int newHeight)
        {
            width = newWidth;
            height = newHeight;

            data = new int[width * height];
        }

        public int this[int x, int y]
        {
            get
            {
                return data[y * width + x];
            }
            set
            {
                data[y * width + x] = value;
            }
        }

        public readonly int width, height;
        public readonly int[] data;
    }

    public class Program
    {
        public static void Main(string[] args)
        {
            ArrayWrapper bigArray = new ArrayWrapper(15000, 15000);

            Stopwatch stopwatch = new Stopwatch();
            stopwatch.Start();
            for (int y = 0; y < bigArray.height; y++)
                for (int x = 0; x < bigArray.width; x++)
                    bigArray.data[y * bigArray.width + x] = 12;
            stopwatch.Stop();

            Console.WriteLine(String.Format("Directly: {0} ms", stopwatch.ElapsedMilliseconds));

            stopwatch.Restart();
            for (int y = 0; y < bigArray.height; y++)
                for (int x = 0; x < bigArray.width; x++)
                    bigArray[x, y] = 12;
            stopwatch.Stop();

            Console.WriteLine(String.Format("Via indexer: {0} ms", stopwatch.ElapsedMilliseconds));

            Console.ReadKey();
        }
    }

許多SO帖子告訴我,程序員應該高度信任優化者來完成它的工作。 但在這種情況下,結果非常令人驚訝:

Directly: 1282 ms
Via indexer: 2134 ms

(在發布配置中編譯並進行優化,我仔細檢查)。

這是一個巨大的差異 - 不是一個統計錯誤(它既可擴展又可重復)。

這是一個非常令人不快的驚喜:在這種情況下,我希望編譯器內聯索引器(它甚至不包括任何范圍檢查),但它沒有這樣做。 這是反匯編(請注意,我的評論是關於正在發生的事情的猜測 ):

直接

                    bigArray.data[y * bigArray.width + x] = 12;
000000a2  mov         eax,dword ptr [ebp-3Ch]  // Evaluate index of array
000000a5  mov         eax,dword ptr [eax+4] 
000000a8  mov         edx,dword ptr [ebp-3Ch] 
000000ab  mov         edx,dword ptr [edx+8] 
000000ae  imul        edx,dword ptr [ebp-10h]  
000000b2  add         edx,dword ptr [ebp-14h]  // ...until here
000000b5  cmp         edx,dword ptr [eax+4]    // Range checking
000000b8  jb          000000BF 
000000ba  call        6ED23CF5                 // Throw IndexOutOfRange
000000bf  mov         dword ptr [eax+edx*4+8],0Ch // Assign value to array

通過索引器

                    bigArray[x, y] = 12;
0000015e  push        dword ptr [ebp-18h]       // Push x and y
00000161  push        0Ch                       // (prepare parameters)
00000163  mov         ecx,dword ptr [ebp-3Ch] 
00000166  mov         edx,dword ptr [ebp-1Ch] 
00000169  cmp         dword ptr [ecx],ecx 
0000016b  call        dword ptr ds:[004B27DCh]  // Call the indexer

(...)

                data[y * width + x] = value;
00000000  push        ebp 
00000001  mov         ebp,esp 
00000003  sub         esp,8 
00000006  mov         dword ptr [ebp-8],ecx 
00000009  mov         dword ptr [ebp-4],edx 
0000000c  cmp         dword ptr ds:[004B171Ch],0 // Some additional checking, I guess?
00000013  je          0000001A 
00000015  call        6ED24648                   
0000001a  mov         eax,dword ptr [ebp-8]        // Evaluating index
0000001d  mov         eax,dword ptr [eax+4] 
00000020  mov         edx,dword ptr [ebp-8] 
00000023  mov         edx,dword ptr [edx+8] 
00000026  imul        edx,dword ptr [ebp+0Ch] 
0000002a  add         edx,dword ptr [ebp-4]        // ...until here
0000002d  cmp         edx,dword ptr [eax+4]        // Range checking
00000030  jb          00000037 
00000032  call        6ED23A5D                     // Throw IndexOutOfRange exception
00000037  mov         ecx,dword ptr [ebp+8] 
0000003a  mov         dword ptr [eax+edx*4+8],ecx  // Actual assignment
                }
0000003e  nop 
0000003f  mov         esp,ebp 
00000041  pop         ebp 
00000042  ret         8                            // Returning

這是一場徹底的災難(就代碼優化而言)。 所以我的問題是:

  • 為什么這段代碼(實際上很簡單)沒有正確優化?
  • 如何修改此代碼,以便根據需要對其進行優化(如果可能)?
  • 程序員可以像C ++那樣依賴C#的優化器嗎?

好吧,我知道,最后一個很難回答。 但是最近我讀了許多關於C ++性能的問題,並且驚訝於優化器可以做多少(例如, std::tie總內聯,兩個std::tuple opeartor <opeartor < on-fly)。


編輯 :(回應評論)

看來,實際上這仍然是我的錯,因為我在運行IDE時檢查了性能。 現在我從IDE運行相同的程序,並通過調試器即時連接到它。 現在我得到:

直接

                    bigArray.data[y * bigArray.width + x] = 12;
000000ae  mov         eax,dword ptr [ebp-10h] 
000000b1  imul        eax,edx 
000000b4  add         eax,ebx 
000000b6  cmp         eax,edi 
000000b8  jae         000001FA 
000000be  mov         dword ptr [ecx+eax*4+8],0Ch 

索引

                    bigArray[x, y] = 12;
0000016b  mov         eax,dword ptr [ebp-14h] 
0000016e  imul        eax,edx 
00000171  add         eax,ebx 
00000173  cmp         eax,edi 
00000175  jae         000001FA 
0000017b  mov         dword ptr [ecx+eax*4+8],0Ch 

這些代碼完全相同(就CPU指令而言)。 運行后,索引器版本實現了比直接版本更好的結果,但只是(我猜)因為緩存。 將測試放入循環后,一切都恢復正常:

Directly: 573 ms
Via indexer: 353 ms
Directly: 356 ms
Via indexer: 362 ms
Directly: 351 ms
Via indexer: 370 ms
Directly: 351 ms
Via indexer: 354 ms
Directly: 359 ms
Via indexer: 356 ms

好; 學過的知識。 即使在發布模式下進行編譯,無論程序是在IDE中運行還是獨立運行,都會產生巨大的差異 謝謝@harold的想法。

運行代碼並立即附加調試器會使其生成慢速代碼(除非您啟用“在模塊加載時禁止JIT優化”,但這會使調試變得有點困難)。 我用來查看優化程序集的過程是拋出一個異常(有條件地說,如果一個靜態變量是0,那么優化器不會太快觸發),並在崩潰時附加調試器。 您可能必須通過“手動選擇調試器”-route。 此外,請確保啟用“顯示外部代碼”(從調用堆棧的上下文菜單中)。

我直接訪問的代碼是

innerloop:
  mov  eax,dword ptr [esi+8]   ; bigArray.width
  imul eax,ebx                 ; * y
  add  eax,edi                 ; + x
  mov  edx,dword ptr [ebp-14h] ; pointer to bigArray.data
  mov  ecx,dword ptr [ebp-10h] ; \
  cmp  eax,ecx                 ; |  bounds check
  jae  00000087                ; /
  mov  dword ptr [edx+eax*4+8],0Ch ; data[index] = 12
  inc  edi                     ; x++
  cmp  edi,dword ptr [esi+8]   ; \
  jl   innerloop               ; / if (x < bigArray.width) goto innerloop

對於索引器:

innerloop:
  mov  eax,dword ptr [esi+8]   ; bigArray.width
  imul eax,ebx                 ; * y
  add  eax,edi                 ; + x
  mov  edx,dword ptr [ebp-14h] ; pointer to bigArray.data
  mov  ecx,dword ptr [ebp-10h] ; \
  cmp  eax,ecx                 ; |  bounds check
  jae  00000087                ; /
  mov  dword ptr [edx+eax*4+8],0Ch ; data[index] = 12
  inc  edi                     ; x++
  cmp  edi,dword ptr [esi+8]   ; \
  jl   innerloop               ; / if (x < bigArray.width) goto innerloop

這不是粘貼錯誤,內循環的代碼確實完全相同

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

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