例如,假設一個變數x,
x可以是任何內容 include 0。
然后我們得到如下代碼:
if(x==0) {
y = 1;
}
else {
y = x;
}
我可以在不使用 C/C 生成分支的情況下做到這一點嗎?
我正在嘗試優化一段代碼。我想盡可能地洗掉分支。有類似的判斷,所以我想把它們轉換成沒有分支的陳述句,以使代碼盡可能高效。
uj5u.com熱心網友回復:
一些一般注意事項:
- 正如其他評論中提到的,一些編譯器可以優化和消除分支。您可以檢查匯編輸出(例如在 Godbolt 中)以確保。
- 提防過早的優化。
- 始終衡量并確保您對占用時間的猜測是正確的。
話雖如此,您可以嘗試以下“技巧”:
y = !x x;
假設x,y是整數型別:
- If
x==0,!xwill be1和ywill 被賦值1。 - If
x!=0,!xwill be0和ywill 被賦值x。
注意:有關標準中的保證,請參閱下面的@CostantinoGrana 評論。您還可以在您的特定環境(編譯器等)中驗證它。
uj5u.com熱心網友回復:
您應該檢查編譯器的匯編器輸出。例如,X86 架構中有一條指令稱為 cmov ( https://www.felixcloutier.com/x86/cmovcc ),旨在處理這種不帶分支的事情。Arm 架構允許根據 CPU 標志執行幾乎所有指令:https ://developer.arm.com/documentation/dui0473/m/condition-codes/conditional-execution-in-arm-state 。
因此,當啟用優化時,您的編譯器可能不會產生任何分支。
uj5u.com熱心網友回復:
如果您需要對輸入的這種非零保證__builtin_clz(對于零輸入給出未定義的結果,否則計算前導零位),一個有用的技巧是y = x|1. 這不會影響任何非零輸入的前導零的數量,因為只有當所有高位都為零時,您才會設定 CLZ 將查看的最后一個位置。如果x=1,那么x|1仍然是1。
與設定__builtin_ctz(計數尾隨零)類似,y = x | (1UL<<31)將設定 MSB。或者更便攜,x | ( 1ULL << (sizeof(x)*CHAR_BIT-1) ).
無條件設定其中一位會改變一些非零值;這僅在某些情況下有用。
要準確獲得您要求的語意,y = x ? x : 1可以這樣表達,或者按照另一個答案x !x中的建議,要求編譯器具體化一個布爾整數并添加它。
當然,智能編譯器有望選擇一種在目標機器上有效的方式。對于 x86,如果可以覆寫,人們可能希望智能編譯器會做這樣的事情x:
cmp eax, 1 ; CF=1 only if EAX==0 because only 0 is < 1U
adc eax, 0 ; x = (eax < 1U)
而對于 AArch64,tst或者cmp然后cinc可以根據任何標志條件有條件地復制和增加一個值。
在Godbolt 編譯器資源管理器上-
int make_nonzero_booleanize(int x){
return x !x;
}
int make_nonzero_ternary(int x){
return x ? x : 1;
}
int make_nonzero_if(int x){
// the original. Compilers may compile it to branchless code
// especially gcc -O3 instead of -O2 is more aggressive about if-conversion
int y;
if (x)
y = x;
else
y = 1;
return y;
}
# GCC12 -O3 for x86-64
make_nonzero_booleanize(int): # return x !x
cmp edi, 1
mov eax, edi
adc eax, 0 # Apparently a GCC dev already thought of this peephole and taught GCC10 about it
# I was expecting compilers to naively do xor-zeroing test/setcc add
ret
make_nonzero_ternary(int):
mov eax, edi
test edi, edi
mov edx, 1
cmove eax, edx # standard ternary using conditional-move
ret
make_nonzero_if(int):
# same asm as ternary; GCC did an if-conversion optimization
// ARM64 gcc12 -O2
make_nonzero_booleanize(int):
cmp w0, 0
cinc w0, w0, eq // return x==0 ? x : x 1
ret
make_nonzero_ternary(int):
cmp w0, 0
csinc w0, w0, wzr, ne // return x==0 ? x : 0 1
ret
make_nonzero_if(int):
// identical to ternary, if-conversion even with -O2
兩種方式都一樣好;即使條件為真并且結果來自遞增零暫存器 ( ) ,csinc結果仍然對輸入具有資料依賴性。w0wzr
# RISC-V 32-bit clang 15 -O3
make_nonzero_booleanize(int):
seqz a1, a0 # tmp = (x==0)
add a0, a0, a1 # return x tmp
ret
make_nonzero_ternary(int):
mv a1, a0 # tmp = x
li a0, 1 # retval = 1
beqz a1, .LBB1_2 # if (x != 0) {
mv a0, a1 # retval = tmp
.LBB1_2: # }
ret # return retval
make_nonzero_if(int):
# identical to ternary, same branching.
Clang 甚至沒有很好地使用它在這里選擇的分支策略。它本可以完成 a bnez,retval=1避免兩個mv指令。除非有一些調優的東西,其中mv一些 RISC-V 微架構將一個分支作為條件移動來處理?希望在行內之后它不會那么糟糕,并且可能會將==0特殊情況的分支目標放在其他地方,它不必跳過它。
x !x在 x86-64 和 RISC-V 上使用一些實際編譯器會更好,并且在 AArch64 上相等。
如果您在某些特定背景關系中使用某些 ISA 的特定編譯器關心這一點,請查看它如何在您的代碼中編譯。(如果你知道什么在 asm 中是有效的。)
較舊的 GCC 版本:
GCC7 和更早的版本使用三元組做得更好,避免使用mov eax, edi, 而是mov eax, 1使用然后使用cmovnz來復制 EDI。希望 GCC8 和更高版本的低效率只發生在不行內這個小函式時;眾所周知,當 GCC 必須解決來自呼叫約定或行內匯編的硬暫存器約束時,它會浪費指令。
GCC9 和更早的版本不知道 x86-64 的cmp/adc技巧,而是使用 的字面實作x !x,不幸的是,這并不是很好,因為 x86 由于setcc.
# GCC9 and earlier -O3
make_nonzero_booleanize(int):
xor eax, eax # zero the full reg
test edi, edi # set FLAGS according to x
sete al # EAX = !x
add eax, edi # EAX = x
ret
公平地說,adc eax, 0在 Sandybridge 之前,Intel 的成本是 2 微秒。(而且 GCC 調整選項可能沒有意識到adc立即 0 是特殊情況;通常adc在 Broadwell 之前在 Intel 上花費 2 微指令。)
x == 0如果非常罕見,無分支可能會更慢
即使在最好的情況下,x86-64、AArch64 和 RISC-V 的無分支代碼也將關鍵路徑依賴鏈延長了 2 條指令,在大多數 CPU 上都具有 1 個周期的延遲。(例如,csinc在 flags 結果準備好之前無法運行cmp。)
但是一個正確預測的分支是一個控制依賴,所以它由 CPU 單獨處理,樂觀地假設y=x會是這種情況,只有在以后檢測到錯誤預測時才會退出。
但是在 C 源代碼中撰寫if()并不能保證你會得到分支匯編。 (不過,與其他撰寫方式相比,that 或 an||更容易編譯分支。)
有關的:
- gcc 優化標志 -O3 使代碼比 -O2 慢- 當分支高度可預測時,GCC 的 x86-64 無分支代碼生成比分支慢的情況。(特別是因為 GCC 使回圈攜帶的依賴鏈比必要的更長。)
-O3在將 if 轉換為無分支方面更具侵略性,但它沒有自動矢量化。 - 為什么條件移動不易受到分支預測失敗的影響?
- http://yarchive.net/comp/linux/cmov.html - Linus Torvalds on
cmov,回到 Pentium 4 天,當時它速度較慢。
uj5u.com熱心網友回復:
也許你可以像在 perl 中那樣做: (void)((y = x) || (y = x 1));
uj5u.com熱心網友回復:
好吧,你總是可以做 (x * x) 1 一個數字的平方永遠不會是負數(除非是虛構的),如果兩者都為零,結果將為 1
轉載請註明出處,本文鏈接:https://www.uj5u.com/caozuo/522242.html
標籤:C编译器优化微优化
上一篇:如何在源檔案中包含所有內容?
下一篇:C宏在函式呼叫中插入引數
