簡體   English   中英

X86 64 位模式下的索引分支開銷

[英]Indexed branch overhead on X86 64 bit mode

這是對先前線程中的一些評論的跟進:

遞歸斐波那契裝配

以下代碼片段計算斐波那契數,第一個示例帶有循環,第二個示例帶有計算跳轉(索引分支)進入展開循環。 這是在帶有 Intel 3770K 3.5ghz 處理器的 Windows 7 Pro 64 位模式下使用 Visual Studio 2015 Desktop Express 進行測試的。 通過 fib(0) 到 fib(93) 的單循環測試,我得到的循環版本的最佳時間是 ~1.901 微秒,計算跳轉的最佳時間是 ~1.324 微秒。 使用外循環將這個過程重復 1,048,576 次,循環版本大約需要 1.44 秒,計算跳轉大約需要 1.04 秒。 在兩組測試中,循環版本比計算跳轉版本慢約 40%。

問題:為什么循環版本比計算跳轉版本對代碼位置更敏感? 在之前的測試中,一些代碼位置組合導致循環版本時間從大約 1.44 秒增加到 1.93 秒,但我從未發現一個組合會顯着影響計算的跳轉版本時間。

部分答案:計算的跳轉版本分支到 280 字節范圍內的 94 個可能的目標位置,顯然分支目標緩沖區(緩存)在優化方面做得很好。 對於循環版本,使用 align 16 將基於程序集的 fib() 函數放在 16 字節邊界上解決了大多數情況下的循環版本時間問題,但對 main() 的一些更改仍然影響時間。 我需要找到一個相當小且可重復的測試用例。

循環版本(注意我讀過 | dec | jnz | 比 | loop | 快):

        align   16
fib     proc                            ;rcx == n
        mov     rax,rcx                 ;br if < 2
        cmp     rax,2
        jb      fib1
        mov     rdx,1                   ;set rax, rdx
        and     rax,rdx
        sub     rdx,rax
        shr     rcx,1
fib0:   add     rdx,rax
        add     rax,rdx
        dec     rcx
        jnz     fib0
fib1:   ret     
fib     endp

計算跳轉(索引分支)到展開循環版本:

        align   16
fib     proc                            ;rcx == n
        mov     r8,rcx                  ;set jmp adr
        mov     r9,offset fib0+279
        lea     r8,[r8+r8*2]
        neg     r8
        add     r8,r9
        mov     rax,rcx                 ;set rax,rdx
        mov     rdx,1
        and     rax,rdx
        sub     rdx,rax
        jmp     r8
fib0:   ; assumes add xxx,xxx takes 3 bytes
        rept    46
        add     rax,rdx
        add     rdx,rax
        endm
        add     rax,rdx
        ret
fib     endp

運行 100 萬 (1048576) 次循環以使用 37%93 的倍數計算fib(0)fib(93)測試代碼,因此順序不是連續的。 在我的系統上,循環版本大約需要 1.44 秒,索引分支版本大約需要 1.04 秒。

#include <stdio.h>
#include <time.h>

typedef unsigned int uint32_t;
typedef unsigned long long uint64_t;

extern "C" uint64_t fib(uint64_t);

/* multiples of 37 mod 93 + 93 at end */
static uint64_t a[94] = 
     {0,37,74,18,55,92,36,73,17,54,
     91,35,72,16,53,90,34,71,15,52,
     89,33,70,14,51,88,32,69,13,50,
     87,31,68,12,49,86,30,67,11,48,
     85,29,66,10,47,84,28,65, 9,46,
     83,27,64, 8,45,82,26,63, 7,44,
     81,25,62, 6,43,80,24,61, 5,42,
     79,23,60, 4,41,78,22,59, 3,40,
     77,21,58, 2,39,76,20,57, 1,38,
     75,19,56,93};

/* x used to avoid compiler optimizing out result of fib() */
int main()
{
size_t i, j;
clock_t cbeg, cend;
uint64_t x = 0;
    cbeg = clock();
    for(j = 0; j < 0x100000; j++)
        for(i = 0; i < 94; i++)
            x += fib(a[i]);
    cend = clock();
    printf("%llx\n", x);
    printf("# ticks = %u\n", (uint32_t)(cend-cbeg));
    return 0;
}

x 的輸出是 0x812a62b1dc000000。 fib(0) 到 fib(93) 的十六進制總和為 0x1bb433812a62b1dc0,再添加 5 個零以循環 0x100000 次:0x1bb433812a62b1dc000000。 由於 64 位數學,高 6 個半字節被截斷。

我制作了一個全匯編版本以更好地控制代碼位置。 循環版本的“if 1”更改為“if 0”。 循環版本大約需要 1.465 到 2.000 秒,具體取決於用於將關鍵位置放在偶數或奇數 16 字節邊界上的 nop 填充(請參閱下面的評論)。 計算的跳躍版本大約需要 1.04 秒,並且邊界在時間上的差異小於 1%。

        includelib msvcrtd
        includelib oldnames

        .data
; multiples of 37 mod 93 + 93 at the end
a       dq      0,37,74,18,55,92,36,73,17,54
        dq     91,35,72,16,53,90,34,71,15,52
        dq     89,33,70,14,51,88,32,69,13,50
        dq     87,31,68,12,49,86,30,67,11,48
        dq     85,29,66,10,47,84,28,65, 9,46
        dq     83,27,64, 8,45,82,26,63, 7,44
        dq     81,25,62, 6,43,80,24,61, 5,42
        dq     79,23,60, 4,41,78,22,59, 3,40
        dq     77,21,58, 2,39,76,20,57, 1,38
        dq     75,19,56,93
        .data?
        .code
;       parameters      rcx,rdx,r8,r9
;       not saved       rax,rcx,rdx,r8,r9,r10,r11
;       code starts on 16 byte boundary
main    proc
        push    r15
        push    r14
        push    r13
        push    r12
        push    rbp
        mov     rbp,rsp
        and     rsp,0fffffffffffffff0h
        sub     rsp,64
        mov     r15,offset a
        xor     r14,r14
        mov     r11,0100000h
;       nop padding effect on loop version (with 0 padding in padx below)
;        0 puts main2 on  odd 16 byte boundary  clk = 0131876622h => 1.465 seconds
;        9 puts main1 on  odd 16 byte boundary  clk = 01573FE951h => 1.645 seconds
        rept    0
        nop
        endm
        rdtsc
        mov     r12,rdx
        shl     r12,32
        or      r12,rax
main0:  xor     r10,r10
main1:  mov     rcx,[r10+r15]
        call    fib
main2:  add     r14,rax
        add     r10,8
        cmp     r10,8*94
        jne     main1
        dec     r11
        jnz     main0
        rdtsc
        mov     r13,rdx
        shl     r13,32
        or      r13,rax
        sub     r13,r12
        mov     rdx,r14
        xor     rax,rax
        mov     rsp,rbp
        pop     rbp
        pop     r12
        pop     r13
        pop     r14
        pop     r15
        ret
main    endp

        align   16
padx    proc
;       nop padding effect on loop version with 0 padding above
;        0 puts fib on  odd 16 byte boundary    clk = 0131876622h => 1.465 seconds
;       16 puts fib on even 16 byte boundary    clk = 01A13C8CB8h => 2.000 seconds
;       nop padding effect on computed jump version with 9 padding above
;        0 puts fib on  odd 16 byte boundary    clk = 00D979792Dh => 1.042 seconds
;       16 puts fib on even 16 byte boundary    clk = 00DA93E04Dh => 1.048 seconds
        rept    0
        nop
        endm
padx    endp

        if      1       ;0 = loop version, 1 = computed jump version

fib     proc                            ;rcx == n
        mov     r8,rcx                  ;set jmp adr
        mov     r9,offset fib0+279
        lea     r8,[r8+r8*2]
        neg     r8
        add     r8,r9
        mov     rax,rcx                 ;set rax,rdx
        mov     rdx,1
        and     rax,rdx
        sub     rdx,rax
        jmp     r8
fib0:   ; assumes add xxx,xxx takes 3 bytes
        rept    46
        add     rax,rdx
        add     rdx,rax
        endm
        add     rax,rdx
        ret
fib     endp

        else

fib     proc                            ;rcx == n
        mov     rax,rcx                 ;br if < 2
        cmp     rax,2
        jb      fib1
        mov     rdx,1                   ;set rax, rdx
        and     rax,rdx
        sub     rdx,rax
        shr     rcx,1
fib0:   add     rdx,rax
        add     rax,rdx
        dec     rcx
        jnz     fib0
fib1:   ret     
fib     endp

        endif
        end

這是對原始問題的回答,即當結果完全未使用時,為什么循環需要計算跳轉版本時間的 1.4 倍。 IDK 究竟為什么用 1 周期add循環攜帶的依賴鏈來累積結果會產生如此大的不同。 嘗試有趣的事情:將它存儲到內存中(例如將它分配給一個volatile int discard ),這樣 asm dep 鏈就不會只是以一個被破壞的寄存器結束。 硬件可能會優化它(例如,一旦確定結果已死,就丟棄 uops)。 英特爾表示 Sandybridge-family 可以為shl reg,cl中的標志結果 uops 之一做到這一點


舊答案:為什么計算的跳轉比結果未使用的循環快 1.4 倍

您在這里測試的是吞吐量,而不是延遲。 在我們之前的討論中,我主要關注延遲。 那可能是個錯誤; 吞吐量對調用者的影響通常與延遲一樣重要,這取決於調用者在執行多少操作后對結果有數據依賴性。

亂序執行隱藏了延遲,因為一次調用的結果不是 arg 到下一次調用的輸入依賴性。 並且 IvyBridge 的亂序窗口足夠大,可以在這里有用: 168-entry ROB(從 issue 到退休),和 54-entry scheduler(從 issue 到 execute) ,以及一個 160-entry 的物理寄存器文件。 另請參閱PRF 與 OOO 窗口大小的 ROB 限制

OOO 執行還隱藏了在任何 Fib 工作完成之前分支錯誤預測的成本。 來自最后一個fib(n) dep 鏈的工作仍在進行中,並且在該錯誤預測期間正在處理中。 (現代 Intel CPU 只回滾到錯誤預測的分支,並且可以在錯誤預測得到解決時繼續從分支之前執行 uops。)

計算分支版本在這里很好是有道理的,因為您主要在 uop 吞吐量方面遇到瓶頸,並且循環退出分支的錯誤預測成本與進入展開版本時的間接分支錯誤預測的成本大致相同。 IvB 可以將sub/jcc宏融合為端口 5 的單個 uop,因此 40% 的數字非常匹配。 (3 個 ALU 執行單元,因此在循環開銷上花費 1/3 或您的 ALU 執行吞吐量解釋了這一點。分支錯誤預測差異和 OOO 執行的限制解釋了其余部分)


我認為在大多數實際用例中,延遲可能是相關的。 也許吞吐量仍然是最重要的,但除此之外的任何事情都會使延遲變得更加重要,因為這甚至根本不使用結果。 當然,在從間接分支錯誤預測中恢復時,管道中會有先前的工作可以處理是正常的,但這會延遲結果准備就緒,這可能意味着如果fib()回報取決於結果。 但是,如果他們不是(如大量的重載和在哪里結果地址的計算),具有前端開始從后發出的微指令fib()遲早是一件好事。

我認為這里的一個很好的中間立場是展開 4 或 8 次,在展開循環之前進行檢查以確保它應該運行一次。 (例如sub rcx,8 / jb .cleanup )。


另請注意,您的循環版本對初始值的n具有數據依賴性。 在我們之前的討論中, 我指出避免這種情況對於亂序執行會更好,因為它讓add鏈在n准備好之前開始工作。 我認為這不是一個重要因素,因為調用者對n延遲很低。 但它確實將循環分支錯誤預測放在n -> fib(n) dep 鏈的末尾而不是中間退出循環。 (如果sub ecx, 2低於零而不是零sub ecx, 2我正在想象循環后的無cmov lea / cmov再進行一次迭代。)

暫無
暫無

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

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