[英]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 ++性能的問題,並且驚訝於優化器可以做多少(例如, 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.