https://www.cnblogs.com/bbqzsl/p/15510377.html
近期在做優化時,對一些函式分別在不同編譯平臺上進行bench測試。發現了不少問題。
現在拿其中一個問題來分享。
1 typedef float MAFloat; 2 3 MAFloat sma(const MAFloat* seq, const int cnt, const int N, const int M) 4 { 5 const MAFloat C1 = (MAFloat)M/N; 6 const MAFloat C2 = (MAFloat)(N-M)/N; 7 MAFloat result = 0.f; 8 int total = cnt; 9 10 #pragma nounroll 11 for (int i = 0; i < total; ++i) 12 { 13 result = result * C2 + seq[i] * C1; 14 } 15 16 return result; 17 }
測試程式碼很簡單,只一個迴圈,迴圈內只做了算術運算,彙編程式碼也很容易。
測試平臺包括:
win10:平臺,vc120,gcc10,clang11
centos8:平臺,gcc8,gcc10,clang11
vc:使用選項 /arch:sse2 /O2,並且win32
gcc:使用選項 -ffast-math -O2 -m32
clang:使用選項 -ffast-math -O2 -m32
陣列長度為 28884 = 7221 * 4;
cpu 是 core i5,3.5Ghz
測試結果:
win10:平臺,vc120 (0.06x ms),gcc10 (0.06x ms),clang11 (0.09x ms)
centos8:平臺,gcc8 (0.06x ms),gcc10 (0.06x ms),clang11 (0.09x ms)
不論在win10還是centos8平臺上,clang編譯的程式碼的效能居然比vc或gcc編譯的程式碼效能差了50%。
現在我們來對比gcc10與clang11產出彙編程式碼
## gcc .L149: movss (%edx,%eax,4), %xmm1 # xmm1 = seq[i] mulss %xmm3, %xmm0 # xmm0 = result * C2 addl $1, %eax # mulss %xmm2, %xmm1 # xmm1 = seq[i] * C1 addss %xmm1, %xmm0 # result = xmm0 + xmm1 cmpl %ecx, %eax jl .L149 # next loop ## clang LBB7_3: # =>This Inner Loop Header: Depth=1 movss (%eax,%edx,4), %xmm4 # xmm4 = mem[0],zero,zero,zero mulss %xmm1, %xmm3 # incl %edx cmpl %ecx, %edx mulss %xmm0, %xmm4 addss %xmm4, %xmm3 mulss %xmm2, %xmm3 # xmm2 = 1/N; jl LBB7_3
gcc生成的彙編程式碼一共7條指令,clang生成的彙編程式碼一共8條指令多出了一條mulss。
clang不知什麼原因自作聰明將
result * C2 + seq[i] * C1;
優化成
(1/N) * (result * (N-M) + seq[i] * M);
即使多出一條mulss指令,效能也不至於差了50%,就像7條指令與10.5條指令的差距。
現在來分析
我的機器使用i5 3.5Ghz, 1ns可以執行3.5指令週期。
陣列長度為28884,即執行迴圈程式碼28884次
執行時間為 28884 * (迴圈體指令週期)/ 3.5
我現在粗略地將每條指令週期看作是1,gcc生成的程式碼執行時間粗略地為 28884 * 7 / 3.5 = 57768ns,與測試結果在0.06ms基本相當。用同樣的方法估算,clang生成的程式碼執行時間粗略地為 28884 * 8 / 3.5 = 66020ns。
但是不同的指令,執行不同數量的微指令(uop),也就是延遲,mulss為4或5,addss為3,上面彙編程式碼的其它指令各為1。
mulss %xmm2, %xmm1 # xmm1 = seq[i] * C1
addss %xmm1, %xmm0 # result = xmm0 + xmm1
在上面兩條指令,addss 依賴 mulss 的結果於 %xmm1,也就是說addss 必須在mulss開始執行後延遲4或5個週期才能執行。由於cpu的亂序機制,這時候延遲的週期數內可以在其他ALU執行其它指令。所以gcc生成的彙編程式碼的情況可以看作沒有指令週期的損失。
再來看clang生成的彙編程式碼
mulss %xmm0, %xmm4 addss %xmm4, %xmm3 mulss %xmm2, %xmm3 # xmm2 = 1/N;
addss 依賴 mulss 的結果於 %xmm4,然後mulss 依賴 addss 的結果於 %xmm3,這裡我們將第一個依賴等同於gcc彙編中的那個依賴,那麼下一個依賴的3個週期就必須等待,一次迴圈一共才8條指令,兩個依賴的延遲合計就8個指令週期,亂序也就沒有指令可以執行,所以就硬生生多出3或4個指令週期等待。
執行時間一下子就變成了 28884 * (8+3) / 3.5 = 90778ns。
估算的結果與測試的結果基本上吻合。
有興趣的朋友可以到godblot上測試彙編,一旦讓clang使用-ffast-math選項,編譯發生這一出傻事。