簡體   English   中英

編寫跨步的x86基准測試

[英]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

但是,這對於處理跨步的addand操作沒有幫助。 例如,上面的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數量不是規則的,它將是nn+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 % 16n*3 % 16 並且n*7 % 32在32次迭代后重復32次。 也就是說,當乘數和模數是相對質數時,線性同余生成器會探索每個小於模數的值。

這些選項都不是理想的,但這是我目前可以建議的最佳選擇。

暫無
暫無

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

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