在二進制機器碼中,比例因子被編碼爲2位移位計數(這就是爲什麼只支持從0到3的2的冪,而不是任意的乘數)。所以機器碼中的[esp+edx]
實際編碼爲[esp+edx*1]
:仍有一個移位量,但它被設置爲0。
移位計數= 0(即,標度係數= 1)不是特殊情況爲硬件,因爲移位是很容易爲硬件做。所以真的,就硬件的內部行爲而言,你的循環都使用相同的尋址模式。
所以@ Ped7g是正確的:你的環路之間的差異,通過使用inc
,而不是add
只是歸結爲節省代碼大小。
實際的加速
見x86標籤維基性能環節,尤其是Agner Fog's guides。
顯然總結的陣列將去SSE2或AVX2向量快得多。使用PADDD。 (因爲你一次需要去16B,所以你不能使用INC和一個比例因子,你可以加4,並使用比例因子4)。
它可以更有效地避免使用一個索引尋址模式。 Intel Sandybridge-family CPUs before Skylake can't micro-fuse indexed addressing modes。在除DEC/JNZ多個CPU
addN PROC
mov ecx, dword ptr [esp+4] ; length
lea edx, [esp+8] ; start of args
lea ecx, [edx + ecx*4] ; end pointer
xor eax, eax ; Accumulator
AdderLoop: ; do{
add eax, dword ptr [edx]
add edx, 4
cmp edx, ecx
jb AdderLoop ; } while(p < endp)
ret
addN ENDP
的add eax, dword ptr [edx]
罐微熔絲連上的SandyBridge,和CMP/JB可以宏觀保險絲。 (AMD和Intel Core2/Nehalem只能融合CMP/JB)。請注意,這會讓我們在循環之外花費額外的指令。
甚至可以減少在循環內的指令的數量,通過向零向上計數,並使用該計數器以指數從陣列的端部。或者,因爲你只是總結的陣列,順序並不重要,你可以循環向下:
addN PROC
mov ecx, dword ptr [esp+4] ; length
xor eax, eax ; Accumulator
AdderLoop: ; do{
add eax, dword ptr [esp+8 + ecx*4-4] ; the +8 and -4 reduce down to just +4, but written this way for clarity.
dec ecx
jnz AdderLoop ; } while(idx != 0)
ret
addN ENDP
因爲現代的x86 CPU可以做到每個時鐘兩個負載,我們只得到了一半吞吐量而不展開。這項技術適用於所有索引方法。
(這其實不是最優的。這表明了計數,向上,向零技術,我前面提到的,我沒有時間來改寫這個認識到循環向下將是最好的了。)
;; untested: unroll by two with a clever way to handle the odd element
addN PROC
mov ecx, dword ptr [esp+4] ; length
lea edx, [esp+8 + ecx*4] ; one-past-the-end
xor eax, eax ; sum1
push esi
xor esi, esi ; sum2
;; Unrolling means extra work to handle the case where the length is odd
shr ecx, 1 ; ecx /= 2, shifting the low bit into CF
cmovc eax, [esp+8] ; sum1 = first element if count was odd
neg ecx ; edx + ecx*8 == 1st or 2nd element
AdderLoop: ; do{
add eax, dword ptr [edx + ecx*8]
add esi, dword ptr [edx + ecx*8 + 4]
inc ecx
jl AdderLoop ; } while(idx < 0)
add eax, esi
pop esi
ret
addN ENDP
在某些CPU上,它的運行速度要快一倍(如果L1緩存中的數據很熱)。使用多個累加器(在這種情況下爲EAX和ESI)對於更高延遲的操作(如FP添加)非常有用。我們在這裏只需要兩個,因爲整數ADD在每個x86微架構上有1個週期延遲。
在Intel之前的Skylake上,使用非索引尋址模式(和add edx, 8
)會更好,因爲每個循環有兩個存儲器尋址操作,但仍然只有一個分支(需要CMP/JB代替通過增加索引來設置測試標誌)。
展開時,它更常見的只使用一個不展開循環來處理第一個或最後一個遺留的迭代。我能夠使用shift和CMOV初始化其中一個累加器,因爲我們只展開2,並且索引尋址模式的比例因子爲8.(我也可以使用and ecx, ~1
來屏蔽ecx以清除低位而不是將其移位,然後用更高的比例因子進行補償。)
恕我直言,性能差異可以忽略這個簡單的循環。在更復雜(算法)的情況下,其他標準將會出現差異。 – zx485