source - glibc版本




為什麼啟用優化後此代碼會慢6.5倍? (2)

我想出於某種原因對 glibcstrlen 函數進行基準測試,並發現它在GCC中啟用優化後表現 慢得多,我不明白為什麼。

這是我的代碼:

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

int main() {
    char *s = calloc(1 << 20, 1);
    memset(s, 65, 1000000);
    clock_t start = clock();
    for (int i = 0; i < 128; ++i) {
        s[strlen(s)] = 'A';
    }
    clock_t end = clock();
    printf("%lld\n", (long long)(end-start));
    return 0;
}

在我的機器上輸出:

$ gcc test.c && ./a.out
13336
$ gcc -O1 test.c && ./a.out
199004
$ gcc -O2 test.c && ./a.out
83415
$ gcc -O3 test.c && ./a.out
83415

不知何故,啟用優化會導致執行時間更長。


Godbolt的Compiler Explorer 上測試代碼提供​​了以下解釋:

  • -O0 或沒有優化時,生成的代碼調用C庫函數 strlen
  • -O1 ,生成的代碼使用 rep scasb 指令使用簡單的內聯擴展。
  • -O2 及以上,生成的代碼使用更精細的內聯擴展。

重複對代碼進行基準測試顯示了從一次運行到另一次運行的重大差異,但增加迭代次數表明:

  • -O1 代碼比C庫實現慢得多: 32240 vs 3090
  • -O2 代碼比 -O1 快,但仍然比C代碼快得多: 8570 vs 3090

此行為特定於 gccglibc 。 使用 clang 和Apple的Libc對OS / X進行的相同測試沒有顯示出顯著的差異,這並不奇怪,因為Godbolt表明 clang 在所有優化級別上生成對C庫 strlen 的調用。

這可能被認為是gcc / glibc中的一個錯誤,但更廣泛的基準測試可能表明調用 strlen 的開銷比小字符串的內聯代碼性能不足更重要。 您進行基準測試的字符串非常大,因此將基準測試重點放在超長字符串上可能無法獲得有意義的結果。

我改進了這個基準並測試了各種字符串長度。 從Linux上的基準測試看來,在英特爾(R)Core(TM)i3-2100 CPU @ 3.10GHz上運行gcc(Debian 4.7.2-5)4.7.2,-O1生成的內聯代碼總是較慢,對於中等長度的弦,其中 -O2 僅比libc strlen快一倍,而對於較長的弦則為 -O2 。 從這些數據來看, strlen 的GNU C庫版本對於大多數字符串長度非常有效,至少在我的特定硬件上是這樣。 還要記住,緩存對基準測量有重大影響。

這是更新的代碼:

#include <stdlib.h>
#include <string.h>
#include <time.h>

void benchmark(int repeat, int minlen, int maxlen) {
    char *s = malloc(maxlen + 1);
    memset(s, 'A', minlen);
    long long bytes = 0, calls = 0;
    clock_t clk = clock();
    for (int n = 0; n < repeat; n++) {
        for (int i = minlen; i < maxlen; ++i) {
            bytes += i + 1;
            calls += 1;
            s[i] = '\0';
            s[strlen(s)] = 'A';
        }
    }
    clk = clock() - clk;
    free(s);
    double avglen = (minlen + maxlen - 1) / 2.0;
    double ns = (double)clk * 1e9 / CLOCKS_PER_SEC;
    printf("average length %7.0f -> avg time: %7.3f ns/byte, %7.3f ns/call\n",
           avglen, ns / bytes, ns / calls);
}

int main() {
    benchmark(10000000, 0, 1);
    benchmark(1000000, 0, 10);
    benchmark(1000000, 5, 15);
    benchmark(100000, 0, 100);
    benchmark(100000, 50, 150);
    benchmark(10000, 0, 1000);
    benchmark(10000, 500, 1500);
    benchmark(1000, 0, 10000);
    benchmark(1000, 5000, 15000);
    benchmark(100, 1000000 - 50, 1000000 + 50);
    return 0;
}

這是輸出:

chqrlie> gcc -std=c99 -O0 benchstrlen.c && ./a.out
average length       0 -> avg time:  14.000 ns/byte,  14.000 ns/call
average length       4 -> avg time:   2.364 ns/byte,  13.000 ns/call
average length      10 -> avg time:   1.238 ns/byte,  13.000 ns/call
average length      50 -> avg time:   0.317 ns/byte,  16.000 ns/call
average length     100 -> avg time:   0.169 ns/byte,  17.000 ns/call
average length     500 -> avg time:   0.074 ns/byte,  37.000 ns/call
average length    1000 -> avg time:   0.068 ns/byte,  68.000 ns/call
average length    5000 -> avg time:   0.064 ns/byte, 318.000 ns/call
average length   10000 -> avg time:   0.062 ns/byte, 622.000 ns/call
average length 1000000 -> avg time:   0.062 ns/byte, 62000.000 ns/call
chqrlie> gcc -std=c99 -O1 benchstrlen.c && ./a.out
average length       0 -> avg time:  20.000 ns/byte,  20.000 ns/call
average length       4 -> avg time:   3.818 ns/byte,  21.000 ns/call
average length      10 -> avg time:   2.190 ns/byte,  23.000 ns/call
average length      50 -> avg time:   0.990 ns/byte,  50.000 ns/call
average length     100 -> avg time:   0.816 ns/byte,  82.000 ns/call
average length     500 -> avg time:   0.679 ns/byte, 340.000 ns/call
average length    1000 -> avg time:   0.664 ns/byte, 664.000 ns/call
average length    5000 -> avg time:   0.651 ns/byte, 3254.000 ns/call
average length   10000 -> avg time:   0.649 ns/byte, 6491.000 ns/call
average length 1000000 -> avg time:   0.648 ns/byte, 648000.000 ns/call
chqrlie> gcc -std=c99 -O2 benchstrlen.c && ./a.out
average length       0 -> avg time:  10.000 ns/byte,  10.000 ns/call
average length       4 -> avg time:   2.000 ns/byte,  11.000 ns/call
average length      10 -> avg time:   1.048 ns/byte,  11.000 ns/call
average length      50 -> avg time:   0.337 ns/byte,  17.000 ns/call
average length     100 -> avg time:   0.299 ns/byte,  30.000 ns/call
average length     500 -> avg time:   0.202 ns/byte, 101.000 ns/call
average length    1000 -> avg time:   0.188 ns/byte, 188.000 ns/call
average length    5000 -> avg time:   0.174 ns/byte, 868.000 ns/call
average length   10000 -> avg time:   0.172 ns/byte, 1716.000 ns/call
average length 1000000 -> avg time:   0.172 ns/byte, 172000.000 ns/call

考慮到 calloc 的16字節對齊,GCC的內聯 strlen 模式比SSE2 pcmpeqb / pmovmskbbsf 要慢得多 。 這種“優化”實際上是一種悲觀。

我使用16字節對齊的簡單手寫循環比gcc -O3 為大緩衝區內聯快5倍,短字符串快2倍。 (並且比調用strlen短字符串更快)。 我已經在 gcc.gnu.org/bugzilla/show_bug.cgi?id=88809 上添加了一條評論來提出這個問題,以便gcc能夠內聯-O2 / -O3。 (如果我們只知道要開始的4字節對齊,建議增加到16字節。)

當gcc知道 緩衝區 有4字節對齊時 (由 calloc 保證),它選擇使用GP整數寄存器( -O2 和更高)將 strlen 內聯爲4字節一次的標量bithack。

(如果我們知道我們不能跨越一個不包含任何字符串字節的頁面,那麼一次讀取4個字節是安全的,因此可能是未映射的。 在同一個緩衝區的末尾讀取是否安全x86和x64上的頁面? (TL:DR是的,在asm中,所以編譯器可以發出代碼,即使在C源代碼中這樣做也是UB。libc strlen 實現也利用了這一點。請參閱我的答案鏈接到glibc strlen 以及如何快速運行大字符串的摘要。)

-O1 ,gcc 總是 (即使沒有已知的對齊)選擇內聯 strlen 作為 repnz scasb ,這非常慢 (在現代Intel CPU上每個時鐘週期大約1個字節)。 rep movs ,“快速字符串”僅適用於 rep stos rep movsrep movs ,而不適用於 repz / repnz 指令。 他們的微碼一次只是簡單的1個字節,但它們仍然有一些啟動開銷。 ( https://agner.org/optimize/

(例如,我們可以通過將指針存儲/重新加載到 volatile void *tmp 來“隱藏”指針從編譯器中進行測試.gcc必須對從 volatile 讀回的指針值進行零假設,從而破壞任何對齊信息。)

GCC確實有一些 x86調優選項, -mstringop-strategy=libcallrep_byte vs. rep_byte 一般用於內聯字符串操作(不僅僅是strlen; memcmp 將是另一個可以用rep或循環完成的主要操作)。 我沒有檢查過這些有什麼影響。

另一個選項的文檔也描述了當前的行為。 我們可以得到這個內聯(使用額外的對齊處理代碼),即使在我們希望它在未對齊的指針上的情況下也是如此。 (這曾經是一個實際的性能,特別是對於小型字符串,在內聯循環不是垃圾的目標上,與機器可以做的相比。)

-minline-all-stringops
默認情況下,僅當已知目標與至少4字節邊界對齊時,GCC才會內聯字符串操作。 這樣可以實現更多內聯並增加代碼大小,但可以提高依賴於快速memcpy,strlen和memset的代碼的性能。

GCC還有你可以用來控制它的 每個函數屬性 ,比如 __attribute__((no-inline-all-stringops)) void foo() { ... } ,但我還沒有玩過它。 (這與inline-all相反。它 並不 意味著內聯 無效 ,它只會回到僅知道4字節對齊時的內聯。)

gcc的內聯 strlen 策略都沒有利用16字節對齊,對x86-64來說非常糟糕

除非小串的情況 常見,做一個4字節的塊,然後對齊的8字節塊的速度大約是4字節的兩倍。

並且4字節策略的清理速度比在包含零字節的雙字內查找字節所需的清理慢得多。 它通過查找具有高位設置的字節來檢測這一點,因此它應該屏蔽其他位並使用 bsf (位掃描前向) 。 這在現代CPU(Intel和Ryzen)上有3個週期延遲。 或者編譯器可以使用 rep bsf 因此它在支持BMI1的CPU上運行為 tzcnt ,這在AMD上更有效。 bsftzcnt 為非零輸入提供相同的結果。

GCC的4字節循環看起來像是從純C編譯的,或者是一些與目標無關的邏輯,而不是利用位掃描。 當使用BMI1編譯x86時,gcc確實使用 andn 來優化它,但每個週期仍然少於4個字節。

對於短輸入和長輸入,SSE2 pcmpeqb + bsf 好得多 。 x86-64保證SSE2可用,並且x86-64 System V具有 alignof(maxalign_t) = 16 因此 calloc 將始終返回至少16字節對齊的指針。

我寫了 strlen 塊的替代品來測試性能

正如預期的那樣,Skylake一次快速增加4倍,而不是4個。

(我使用 -O3 將原始源代碼編譯為asm,然後編輯asm,看看這個策略應該用於 strlen 內聯擴展。我還將它移植到C源代碼中的內聯asm; 在Godbolt上查看該版本 。 )

    # at this point gcc has `s` in RDX, `i` in ECX

    pxor       %xmm0, %xmm0         # zeroed vector to compare against
    .p2align 4
.Lstrlen16:                         # do {
#ifdef __AVX__
    vpcmpeqb   (%rdx), %xmm0, %xmm1
#else
    movdqa     (%rdx), %xmm1
    pcmpeqb    %xmm0, %xmm1           # xmm1 = -1 where there was a 0 in memory
#endif

    add         $16, %rdx             # ptr++
    pmovmskb  %xmm1, %eax             # extract high bit of each byte to a 16-bit mask
    test       %eax, %eax
    jz        .Lstrlen16            # }while(mask==0);
    # RDX points at the 16-byte chunk *after* the one containing the terminator
    # EAX = bit-mask of the 0 bytes, and is known to be non-zero
    bsf        %eax, %eax           # EAX = bit-index of the lowest set bit

    movb       $'A', -16(%rdx, %rax)

請注意,我將strlen清理的一部分優化為存儲尋址模式:我使用 -16 位移校正過衝,並且這只是找到字符串的結尾,而不是實際計算長度,然後像GCC一樣索引已經在內聯其4字節一次循環後進行操作。

要獲得實際的字符串 長度 (而不是指向結尾的指針),你需要減去rdx-start然後添加 rax-16 (可能有一個LEA來添加2個寄存器+一個常量,但是3個組件的LEA有更多的延遲。)

使用AVX允許在一條指令中進行加載+比較而不破壞歸零寄存器,整個循環僅為4微秒,低於5.(測試/ jz宏融合到Intel和AMD上的一個 vpcmpeqb 。帶有 非索引的 vpcmpeqb memory-source可以通過整個管道保持微融合,因此前端只有1個融合域uop。)

(請注意,將128位AVX與SSE混合即使在Haswell上也 不會 造成停頓,只要你處於乾淨的上部狀態就可以了。所以我沒有費心將其他指令更改為AVX,只有一個對於AVX循環體而言, pxor 實際上比我的桌面上的 pxor 稍微 一些,但似乎有一些小的影響。它似乎有點可重複,但它很奇怪,因為它沒有代碼大小差異,因此沒有對齊差異。)

pmovmskb 是一個單指令。 它在英特爾和Ryzen上有3個週期的延遲(更糟糕的是Bulldozer系列)。 對於短字符串,通過SIMD單元並返回整數的行程是關鍵路徑依賴鏈的重要部分,用於從輸入存儲器字節到存儲地址就緒的延遲。 但只有SIMD具有打包整數比較,因此標量必須做更多的工作。

對於非常小的字符串情況(例如0到3個字節),通過使用純標量(特別是在Bulldozer系列上),可能會通過使用 0到15個字節的所有字符串 來實現稍微更低的延遲。 相同的分支路徑(循環分支從不採用)對於大多數短字符串用例非常好

當我們知道我們有16字節對齊時,對於高達15字節的所有字符串都非常有用似乎是一個不錯的選擇。 更可預測的分支是非常好的。 (並註意,當循環時, pmovmskb 延遲僅影響我們檢測分支錯誤預測的速度,以突破循環;分支預測+推測執行會在每次迭代中隱藏獨立pmovmskb的延遲。

如果我們期望更長的字符串是常見的,我們可以展開一點,但在那時你應該只調用libc函數,以便它可以在運行時調度到AVX2。 展開超過1個向量會使清理變得複雜,從而損害了簡單的情況。

在我的機器i7-6700k Skylake上, energy_performance_preference 最大turbo(和 energy_performance_preference =性能),在Arch Linux上使用gcc8.2,我獲得了一些一致的基準時序,因為我的CPU時鐘速度在memset期間加速。 但也許並不總是最大的渦輪增壓; Skylake的hw電源管理在內存受限時會下降。 perf stat 顯示我通常在4.0GHz左右運行時平均stdout輸出並在stderr上查看perf摘要。

perf stat -r 100 ./a.out | awk '{sum+= $1}  END{print sum/100;}'

我最終將我的asm複製到GNU C inline-asm語句中, 因此我可以將代碼放在Godbolt編譯器資源管理器上

對於大字符串,長度與問題相同:在~4GHz Skylake上的時間

  • ~62100 clock_t 時間單位:-O1 rep scas :( clock() 有點過時,但我沒有打擾它。)
  • ~15900 clock_t 時間單位: -O3 gcc 4字節循環策略:平均100次運行=。 (或者〜-15800 with -march=native for andn
  • ~1880 clock_t 時間單位: -O3 使用glibc strlen 函數調用,使用AVX2
  • ~3190 clock_t 時間單位:( AVX1 128位向量,4 uop循環)手寫內聯asm,gcc可以/應該內聯。
  • ~3230 clock_t 時間單位:( SSE2 5 uop loop)手寫內聯asm,gcc可以/應該內聯。

我的手寫asm對短串也應該非常好,因為它不需要特別分支。 已知的對齊對於strlen 非常 有用,而libc無法利用它。

如果我們期望大字符串很少,那麼比libc慢1.7倍。 1M字節的長度意味著它不會在我的CPU上的L2(256k)或L1d緩存(32k)中保持熱,因此即使在L3緩存上存在瓶頸,libc版本也會更快。 (可能是一個展開的循環和256位向量不會阻塞每個字節有多少uop的ROB,所以OoO exec可以看得更遠,並獲得更多的內存並行性,尤其是在頁面邊界。)

但L3緩存帶寬可能是阻止4-uop版本每時鐘以1次迭代運行的瓶頸,因此我們看到AVX在循環中節省了大量優勢。 隨著L1d緩存中的數據熱,我們應該每次迭代獲得1.25個週期,而不是1。

但是一個好的AVX2實現可以在每個週期讀取多達64個字節(2x 32字節加載),使用 vpminub 組合對,然後檢查零並返回查找它們的位置。 對於大約2k到~30kiB左右,這個和libc之間的間隙開闊,在L1d中保持熱。

一些長度為1000的只讀測試表明glibc strlen 在L1d緩存中對於中等大小的字符串來說真的比我的循環快4倍 。 這足以讓AVX2升級到大的展開循環,但仍然很容易適應L1d緩存。 (只讀避免存儲轉發停頓,因此我們可以進行多次迭代)

如果你的字符串很大,你應該使用顯式長度的字符串而不是需要 strlen ,所以內聯一個簡單的循環似乎仍然是一個合理的策略,只要它實際上對短字符串 有好處 而不是對於中等字符串的總垃圾(如300字節)和非常長(>緩存大小)字符串。

用這個標記小字符串:

我試圖獲得我期望的結果時遇到了一些奇怪的事情:

我嘗試 s[31] = 0 在每次迭代之前截斷字符串(允許短的常量長度)。 但後來我的SSE2版本與GCC的版本幾乎相同。 商店轉發攤位是瓶頸! 字節存儲後跟更寬的加載使得存儲轉發採用慢速路徑,該路徑將來自存儲緩衝區的字節與來自L1d高速緩存的字節合併。 這個額外延遲是通過字符串的最後4字節或16字節塊的循環傳輸dep鏈的一部分,以計算下一次迭代的存儲索引。

GCC較慢的4字節一次代碼可以通過在該延遲的陰影下處理早期的4字節塊來保持同步。 (亂序執行非常棒:慢速代碼有時不會影響程序的整體速度)。

我最終通過製作只讀版本來解決它,並使用內聯asm來阻止編譯器將 strlen 提升出循環。

但是存儲轉發是使用16字節加載的潛在問題。 如果其他C變量存儲在數組的末尾之外,我們可能會因為加載數組末端而不是使用較窄的存儲而觸發SF停頓。 對於最近複製的數據,如果使用16字節或更寬的對齊存儲複製它們,我們會很好,但對於小複製的glibc memcpy,它會覆蓋整個對象的2倍重疊負載,從對象的開始和結束開始。 然後它存儲兩個,再次重疊,免費處理memmove src重疊dst情況。 因此,只是memcpyied的短字符串的第二個16字節或8字節塊可能會給我們一個SF停頓來讀取最後一個塊。 (具有輸出數據依賴性的那個。)

只是跑得慢,所以你沒有在它準備好之前就結束了一般不好,所以這裡沒有很好的解決方案。 我想 大部分 時間你都不打算 一個你剛才 的緩衝區, 通常你會打算輸入一個你只讀的輸入,所以存儲轉發停頓不是問題 。 如果其他東西只是寫了它,那麼有效的代碼希望不會拋棄長度並調用一個需要重新計算它的函數。

其他奇怪我還沒有完全弄明白:

代碼對齊使得只讀因子為2,size = 1000( s[1000] = 0; )。 但最內層的asm循環本身與 .p2align 4.p2align 5 對齊。 增加循環對齊可以將其減慢2倍!

# slow version, with *no* extra HIDE_ALIGNMENT function call before the loop.
# using my hand-written asm, AVX version.
  i<1280000 read-only at strlen(s)=1000 so strlen time dominates the total runtime (not startup overhead)
  .p2align 5 in the asm inner loop. (32-byte code alignment with NOP padding)

gcc -DUSE_ASM -DREAD_ONLY -DHIDE_ALIGNMENT -march=native -O3 -g strlen-microbench.c &&
 time taskset -c 3 perf stat -etask-clock,context-switches,cpu-migrations,page-faults,cycles,branches,branch-misses,instructions,uops_issued.any,uops_executed.thread -r 100 ./a.out |
 awk '{sum+= $1}  END{print sum/100;}'

 Performance counter stats for './a.out' (100 runs):

             40.92 msec task-clock                #    0.996 CPUs utilized            ( +-  0.20% )
                 2      context-switches          #    0.052 K/sec                    ( +-  3.31% )
                 0      cpu-migrations            #    0.000 K/sec                  
               313      page-faults               #    0.008 M/sec                    ( +-  0.05% )
       168,103,223      cycles                    #    4.108 GHz                      ( +-  0.20% )
        82,293,840      branches                  # 2011.269 M/sec                    ( +-  0.00% )
         1,845,647      branch-misses             #    2.24% of all branches          ( +-  0.74% )
       412,769,788      instructions              #    2.46  insn per cycle           ( +-  0.00% )
       466,515,986      uops_issued.any           # 11401.694 M/sec                   ( +-  0.22% )
       487,011,558      uops_executed.thread      # 11902.607 M/sec                   ( +-  0.13% )

         0.0410624 +- 0.0000837 seconds time elapsed  ( +-  0.20% )

40326.5   (clock_t)

real    0m4.301s
user    0m4.050s
sys     0m0.224s

注意分支未命中肯定是非零,而快速版本幾乎完全為零。 發布的uops遠遠高於快速版本:它可能在 很長一段 時間內對這些分支未命中的錯誤路徑進行推測。

可能內部和外部環路分支彼此混疊或不混合。

指令計數幾乎相同,只是內環之前的外環中的一些NOP不同。 但IPC有很大的不同:沒有問題,快速版本每個時鐘平均運行4.82條指令用於整個程序。 (大多數是在每個週期運行5個指令的最內層循環中,這要歸功於test / jz將2個指令宏融合到1個uop中。)並註意uops_executed遠高於uops_issued:這意味著微融合是通過前端瓶頸工作得更好。

fast version, same read-only strlen(s)=1000 repeated 1280000 times

gcc -DUSE_ASM -DREAD_ONLY -UHIDE_ALIGNMENT -march=native -O3 -g strlen-microbench.c &&
 time taskset -c 3 perf stat -etask-clock,context-switches,cpu-migrations,page-faults,cycles,branches,branch-misses,instructions,uops_issued.any,uops_executed.thread -r 100 ./a.out |
 awk '{sum+= $1}  END{print sum/100;}' 

 Performance counter stats for './a.out' (100 runs):

             21.06 msec task-clock                #    0.994 CPUs utilized            ( +-  0.10% )
                 1      context-switches          #    0.056 K/sec                    ( +-  5.30% )
                 0      cpu-migrations            #    0.000 K/sec                  
               313      page-faults               #    0.015 M/sec                    ( +-  0.04% )
        86,239,943      cycles                    #    4.094 GHz                      ( +-  0.02% )
        82,285,261      branches                  # 3906.682 M/sec                    ( +-  0.00% )
            17,645      branch-misses             #    0.02% of all branches          ( +-  0.15% )
       415,286,425      instructions              #    4.82  insn per cycle           ( +-  0.00% )
       335,057,379      uops_issued.any           # 15907.619 M/sec                   ( +-  0.00% )
       409,255,762      uops_executed.thread      # 19430.358 M/sec                   ( +-  0.00% )

         0.0211944 +- 0.0000221 seconds time elapsed  ( +-  0.10% )

20504  (clock_t)

real    0m2.309s
user    0m2.085s
sys     0m0.203s

我認為這只是分支預測,而不是其他前端問題。 測試/分支指令不會跨越阻止宏融合的邊界。

.p2align 5 更改為 .p2align 4 將它們反轉: -UHIDE_ALIGNMENT 變慢。

這個Godbolt二進制鏈接 再現了我在Arch Linux上用gcc8.2.1看到的相同填充兩種情況:2x 11字節 nopw +外部循環中的3字節 nop 用於快速情況。 它也有我在本地使用的確切來源。

短strlen只讀微基準:

使用選擇的內容進行測試,使其不會受到分支錯誤預測或存儲轉發的影響,並且可以重複測試相同的短長度以進行足夠的迭代以獲得有意義的數據。

strlen=33 ,因此終結符接近第3個16字節向量的開頭。 (使我的版本看起來盡可能與4字節版本相比。)- -DREAD_ONLY ,並且 i<1280000 作為外循環重複循環。

  • 1933 clock_t:我的asm :漂亮且一致的最佳情況時間(在重新運行平均值時沒有嘈雜/彈跳。)與較長的strlen不同,有/無 -DHIDE_ALIGNMENT 。 使用更短的模式可以更容易地預測循環分支。 (strlen = 33,而不是1000)。
  • 3220 clock_t:gcc -O3 strlen 。 ( -DHIDE_ALIGNMENT
  • 6100 clock_t:gcc -O3 4字節循環
  • 37200 clock_t:gcc -O1 repz scasb

所以對於短字符串,我的簡單內聯循環 擊敗 了對 strlen 的庫函數調用,它必須通過PLT(調用+ jmp [mem] ),然後運行strlen的啟動開銷,而不依賴於對齊。

對於 strlen(s)=33 所有版本,可忽略不計的分支錯誤預測,如0.05%。 repz scasb版本有0.46%,但總分支數較少。 沒有內環來架起許多正確預測的分支。

對於分支預測器和代碼緩存熱, repz scasb 比調用glibc strlen 一個33字節的字符串差10倍。 strlen 可能會錯過甚至錯過代碼緩存和失速的實際使用情況下會更糟糕,但是直線 repz scasb 不會。 但是10倍是巨大的,這是一個相當短的字符串。





glibc