[英]Writing a strided x86 benchmark
我想編寫一個負載基准,以已知的編譯時跨度跨給定的內存區域,並以盡可能少的非負載指令包裝在區域的末尾(2的冪)。
例如,步幅為4099, rdi
的迭代計數和rsi
的指向內存區域的指針是“有效的”:
%define STRIDE 4099
%define SIZE 128 * 1024
%define MASK (SIZE - 1)
xor ecx, ecx
.top:
mov al, [rsi + rcx]
add ecx, STRIDE
and ecx, MASK
dec rdi
jnz .top
問題在於,有4條非加載指令僅用於支持單個加載,處理步幅加法,掩碼和循環終止檢查。 此外, ecx
還帶有一個2周期依賴鏈。
我們可以稍微展開一下,以將循環終止檢查成本降低到接近零,並分解依賴關系鏈(此處展開4倍):
.top:
lea edx, [ecx + STRIDE * 0]
and edx, MASK
movzx eax, BYTE [rsi + rdx]
lea edx, [ecx + STRIDE * 1]
and edx, MASK
movzx eax, BYTE [rsi + rdx]
lea edx, [ecx + STRIDE * 2]
and edx, MASK
movzx eax, BYTE [rsi + rdx]
lea edx, [ecx + STRIDE * 3]
and edx, MASK
movzx eax, BYTE [rsi + rdx]
add ecx, STRIDE * 4
dec rdi
jnz .top
但是,這對於處理跨步的add
和and
操作沒有幫助。 例如,上面的benmark報告L1包含區域為0.875個循環/負載,但是我們知道正確的答案是0.5(每個循環兩個負載)。 0.875來自15個總uops / 4 uops周期,即,我們受uop吞吐量的4寬最大寬度約束,而不是負載吞吐量。
關於如何有效展開循環以消除跨步計算的大部分成本的任何想法?
為了“絕對最大的精神錯亂”; 您可以要求操作系統在許多虛擬地址上映射相同的頁面(例如,使相同的16 MiB RAM出現在虛擬地址0x100000000、0x11000000、0x12000000、0x13000000等),以避免需要進行換行; 您可以使用自行生成的代碼來避免其他一切。 基本上,生成如下指令的代碼:
movzx eax, BYTE [address1]
movzx ebx, BYTE [address2]
movzx ecx, BYTE [address3]
movzx edx, BYTE [address4]
movzx esi, BYTE [address5]
movzx edi, BYTE [address6]
movzx ebp, BYTE [address7]
movzx eax, BYTE [address8]
movzx ebx, BYTE [address9]
movzx ecx, BYTE [address10]
movzx edx, BYTE [address11]
...
movzx edx, BYTE [address998]
movzx esi, BYTE [address999]
ret
當然,所有使用的地址都將由生成指令的代碼來計算。
注意:根據具體的CPU,有一個循環而不是完全展開可能更快(在指令獲取和解碼成本與循環開銷之間進行折衷)。 對於較新的Intel CPU,有一種稱為“循環流檢測器”的東西,旨在避免對小於特定大小(該大小取決於CPU型號)的循環進行獲取和解碼。 並且我認為生成適合該大小的循環是最佳的。
關於那個數學。 證明...在展開循環的開頭,如果ecx < STRIDE
,並且n = (SIZE div STRIDE)
,並且SIZE無法被STRIDE整除,則(n-1)*STRIDE < SIZE
,即n-1次迭代安全無遮擋。 第n次迭代可能並且可能不需要屏蔽(取決於初始ecx
)。 如果第n次迭代不需要掩碼,則第(n + 1)次需要掩碼。
結果是,您可以像這樣設計代碼
xor ecx, ecx
jmp loop_entry
unrolled_loop:
and ecx, MASK ; make ecx < STRIDE again
jz terminate_loop
loop_entry:
movzx eax, BYTE [rsi+rcx]
add ecx, STRIDE
movzx eax, BYTE [rsi+rcx]
add ecx, STRIDE
movzx eax, BYTE [rsi+rcx]
add ecx, STRIDE
... (SIZE div STRIDE)-1 times
movzx eax, BYTE [rsi+rcx]
add ecx, STRIDE
;after (SIZE div STRIDE)-th add ecx,STRIDE
cmp ecx, SIZE
jae unrolled_loop
movzx eax, BYTE [rsi+rcx]
add ecx, STRIDE
; assert( ecx >= SIZE )
jmp unrolled_loop
terminate_loop:
之前and
需要發生的add
數量不是規則的,它將是n
或n+1
,因此展開循環的末尾已加倍,以ecx < STRIDE
值開始每個展開的循環。
我對nasm宏不好,不能決定是否可以通過某種宏魔術來展開它。
還有一個問題是否可以將其宏化到不同的寄存器,例如
xor ecx, ecx
...
loop_entry:
lea rdx,[rcx + STRIDE*4]
movzx eax, BYTE [rsi + rcx]
movzx eax, BYTE [rsi + rcx + STRIDE]
movzx eax, BYTE [rsi + rcx + STRIDE*2]
movzx eax, BYTE [rsi + rcx + STRIDE*3]
add ecx, STRIDE*8
movzx eax, BYTE [rsi + rdx]
movzx eax, BYTE [rsi + rdx + STRIDE]
movzx eax, BYTE [rsi + rdx + STRIDE*2]
movzx eax, BYTE [rsi + rdx + STRIDE*3]
add edx, STRIDE*8
...
then the final part can be filled with simple
movzx eax, BYTE [rsi + rcx]
add ecx, STRIDE
... until the n-th ADD state is reached, then jae loop final
;after (SIZE div STRIDE)-th add ecx,STRIDE
cmp ecx, SIZE
jae unrolled_loop
movzx eax, BYTE [rsi + rcx]
add ecx, STRIDE
; assert( ecx >= SIZE )
jmp unrolled_loop
內部的“安全”部分也可以循環一些,例如,如果您的示例中SIZE div STRIDE = 31.97657965357404,那么內部的8倍movzx
可以循環3次... 3 * 8 = 24,然后是7倍的非循環-和簡單的行達到31x add
,然后根據需要加倍循環出口,最終達到32nd add
。
盡管在您的31.9的情況下看起來毫無意義,但在類似數百+ = SIZE div STRIDE的情況下循環中間部分是有意義的。
如果使用AVX2收集來生成載荷,則可以將SIMD用於添加+與。 但是,當嘗試測量有關非聚集負載的任何內容時,這可能並不是您想要的!
如果您的區域大小為2 ^ 16..19,則可以使用add ax, dx
(DX = stride以避免LCP停頓)免費獲得2 ^ 16的環繞效果。 使用eax
作為縮放索引。 使用lea di, [eax + STRIDE * n]
等在展開的循環中,這樣可以節省足夠的微指令,使您每個時鍾運行2個負載而不會在前端造成瓶頸。 但是, 部分寄存器合並依賴項(在Skylake上)將創建多個循環傳送的dep鏈,如果需要避免重復使用它們,則會以32位模式用盡寄存器。
您甚至可以考慮映射低64k的虛擬內存(在Linux上設置vm.mmap_min_addr=0
),並在32位代碼中使用16位尋址模式。 僅讀取16位寄存器避免了僅寫入16位的麻煩。 最好在鞋幫16中放入垃圾。
為了在沒有16位尋址模式的情況下做得更好,您需要創建知道無法發生換行的條件 。 這允許使用[reg + STRIDE * n]
尋址模式展開。
您可以編寫一個正常的展開循環,該循環在接近折返點時會中斷(例如,當ecx + STRIDE*n > bufsize
),但是如果bufsize / STRIDE在Skylake上大於約22,則無法很好預測。
您可以每次迭代僅進行一次AND屏蔽,並放寬工作集正好為 2 ^ n個字節的約束。 例如,如果您保留了足夠的空間來使負載超出末尾,直到STRIDE * n - 1
,並且您對這種緩存行為沒問題,那就去做吧。
如果您仔細選擇展開因子,則可以控制每次環繞的發生位置。 但是,如果有一個大跨度和2的冪,我認為您需要展開lcm(stride, bufsize/stride) = stride * bufsize/stride = bufsize
才能重復該模式。 對於不適合L1的緩沖區大小,此展開因子太大而無法放入uop緩存,甚至L1I。 我看了幾個小型測試用例,例如n*7 % 16
,它在16次迭代后重復執行,就像n*5 % 16
和n*3 % 16
。 並且n*7 % 32
在32次迭代后重復32次。 也就是說,當乘數和模數是相對質數時,線性同余生成器會探索每個小於模數的值。
這些選項都不是理想的,但這是我目前可以建議的最佳選擇。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.