[英]Why does this code snippet produce radically different assembly code in C and C++?
[英]Why does GCC generate such radically different assembly for nearly the same C code?
在編寫優化的ftol
函數時,我在GCC 4.6.1
發現了一些非常奇怪的行為。 讓我先向您展示代碼(為清楚起見,我標記了差異):
fast_trunc_one,C:
int fast_trunc_one(int i) {
int mantissa, exponent, sign, r;
mantissa = (i & 0x07fffff) | 0x800000;
exponent = 150 - ((i >> 23) & 0xff);
sign = i & 0x80000000;
if (exponent < 0) {
r = mantissa << -exponent; /* diff */
} else {
r = mantissa >> exponent; /* diff */
}
return (r ^ -sign) + sign; /* diff */
}
fast_trunc_two,C:
int fast_trunc_two(int i) {
int mantissa, exponent, sign, r;
mantissa = (i & 0x07fffff) | 0x800000;
exponent = 150 - ((i >> 23) & 0xff);
sign = i & 0x80000000;
if (exponent < 0) {
r = (mantissa << -exponent) ^ -sign; /* diff */
} else {
r = (mantissa >> exponent) ^ -sign; /* diff */
}
return r + sign; /* diff */
}
好像是對的嗎? GCC不同意。 用gcc -O3 -S -Wall -o test.s test.c
編譯后,這是程序集輸出:
fast_trunc_one,生成:
_fast_trunc_one:
LFB0:
.cfi_startproc
movl 4(%esp), %eax
movl $150, %ecx
movl %eax, %edx
andl $8388607, %edx
sarl $23, %eax
orl $8388608, %edx
andl $255, %eax
subl %eax, %ecx
movl %edx, %eax
sarl %cl, %eax
testl %ecx, %ecx
js L5
rep
ret
.p2align 4,,7
L5:
negl %ecx
movl %edx, %eax
sall %cl, %eax
ret
.cfi_endproc
fast_trunc_two,生成:
_fast_trunc_two:
LFB1:
.cfi_startproc
pushl %ebx
.cfi_def_cfa_offset 8
.cfi_offset 3, -8
movl 8(%esp), %eax
movl $150, %ecx
movl %eax, %ebx
movl %eax, %edx
sarl $23, %ebx
andl $8388607, %edx
andl $255, %ebx
orl $8388608, %edx
andl $-2147483648, %eax
subl %ebx, %ecx
js L9
sarl %cl, %edx
movl %eax, %ecx
negl %ecx
xorl %ecx, %edx
addl %edx, %eax
popl %ebx
.cfi_remember_state
.cfi_def_cfa_offset 4
.cfi_restore 3
ret
.p2align 4,,7
L9:
.cfi_restore_state
negl %ecx
sall %cl, %edx
movl %eax, %ecx
negl %ecx
xorl %ecx, %edx
addl %edx, %eax
popl %ebx
.cfi_restore 3
.cfi_def_cfa_offset 4
ret
.cfi_endproc
這是一個極端的差異。 這實際上也出現在配置文件中, fast_trunc_one
比fast_trunc_two
快30% fast_trunc_two
。 現在我的問題是:是什么導致這個?
已更新以與OP的編輯同步
通過修改代碼,我已經設法看到GCC如何優化第一種情況。
在我們理解為什么它們如此不同之前,首先我們必須了解GCC如何優化fast_trunc_one()
。
信不信由你, fast_trunc_one()
正在優化:
int fast_trunc_one(int i) {
int mantissa, exponent;
mantissa = (i & 0x07fffff) | 0x800000;
exponent = 150 - ((i >> 23) & 0xff);
if (exponent < 0) {
return (mantissa << -exponent); /* diff */
} else {
return (mantissa >> exponent); /* diff */
}
}
這將生成與原始fast_trunc_one()
完全相同的程序集 - 注冊名稱和所有內容。
請注意, fast_trunc_one()
的程序fast_trunc_one()
沒有xor
。 這就是為我帶來的東西。
第1步: sign = -sign
首先,我們來看看sign
變量。 由於sign = i & 0x80000000;
, sign
只有兩個可能的值:
sign = 0
sign = 0x80000000
現在認識到,在這兩種情況下, sign == -sign
。 因此,當我將原始代碼更改為:
int fast_trunc_one(int i) {
int mantissa, exponent, sign, r;
mantissa = (i & 0x07fffff) | 0x800000;
exponent = 150 - ((i >> 23) & 0xff);
sign = i & 0x80000000;
if (exponent < 0) {
r = mantissa << -exponent;
} else {
r = mantissa >> exponent;
}
return (r ^ sign) + sign;
}
它生成與原始fast_trunc_one()
完全相同的程序集。 我會為你免費組裝,但它是相同的 - 注冊名稱和所有。
步驟2:數學簡化: x + (y ^ x) = y
sign
只能取兩個值中的一個, 0
或0x80000000
。
x = 0
,那么x + (y ^ x) = y
然后是平凡的。 0x80000000
添加和xoring是相同的。 它翻轉了標志位。 因此,當x = 0x80000000
時, x + (y ^ x) = y
也成立。 因此, x + (y ^ x)
減少到y
。 代碼簡化為:
int fast_trunc_one(int i) {
int mantissa, exponent, sign, r;
mantissa = (i & 0x07fffff) | 0x800000;
exponent = 150 - ((i >> 23) & 0xff);
sign = i & 0x80000000;
if (exponent < 0) {
r = (mantissa << -exponent);
} else {
r = (mantissa >> exponent);
}
return r;
}
再次,這編譯成完全相同的程序集 - 寄存器名稱和所有。
以上版本最終簡化為:
int fast_trunc_one(int i) {
int mantissa, exponent;
mantissa = (i & 0x07fffff) | 0x800000;
exponent = 150 - ((i >> 23) & 0xff);
if (exponent < 0) {
return (mantissa << -exponent); /* diff */
} else {
return (mantissa >> exponent); /* diff */
}
}
這幾乎就是GCC在裝配中生成的內容。
那么為什么編譯器不能將fast_trunc_two()
優化為同樣的東西呢?
fast_trunc_one()
的關鍵部分是x + (y ^ x) = y
優化。 在fast_trunc_two()
, x + (y ^ x)
表達式在分支上被拆分。
我懷疑這可能足以讓GCC混淆不進行優化。 (它需要將^ -sign
提升出分支並將其合並到末尾的r + sign
中。)
例如,這會生成與fast_trunc_one()
相同的程序集:
int fast_trunc_two(int i) {
int mantissa, exponent, sign, r;
mantissa = (i & 0x07fffff) | 0x800000;
exponent = 150 - ((i >> 23) & 0xff);
sign = i & 0x80000000;
if (exponent < 0) {
r = ((mantissa << -exponent) ^ -sign) + sign; /* diff */
} else {
r = ((mantissa >> exponent) ^ -sign) + sign; /* diff */
}
return r; /* diff */
}
這是編譯器的本質。 假設他們將采取最快或最好的路徑,是非常錯誤的。 任何暗示你不需要對你的代碼做任何事情來優化的人,因為“現代編譯器”填補空白,做最好的工作,制作最快的代碼等。實際上我看到gcc從3.x變得更糟。 x至少在手臂上。 到目前為止,4.x可能已達到3.x,但在早期它會產生較慢的代碼。 通過練習,您可以學習如何編寫代碼,這樣編譯器就不必努力工作,從而產生更一致和預期的結果。
這里的錯誤是你對將要產生的東西的期望,而不是實際產生的東西。 如果希望編譯器生成相同的輸出,請為其輸入相同的輸入。 不是數學上相同,不是有點相同,但實際上是相同的,沒有不同的路徑,沒有從一個版本到另一個版本的共享或分發操作。 這是一個很好的練習,可以理解如何編寫代碼並查看編譯器使用它做什么。 不要錯誤地假設因為一天的一個處理器目標的一個版本的gcc產生了一定的結果,這是所有編譯器和所有代碼的規則。 您必須使用許多編譯器和許多目標來了解正在發生的事情。
gcc非常討厭,我邀請你看看幕后,看看gcc的膽量,嘗試添加一個目標或自己修改一些東西。 它幾乎沒有用膠帶和撈絲固定在一起。 在關鍵位置添加或刪除了一行額外的代碼,它崩潰了。 事實上,它已經產生了可用的代碼,這是令人高興的事情,而不是擔心為什么它沒有滿足其他期望。
你看過gcc不同版本的產品嗎? 3.x和4.x特別是4.5 vs 4.6 vs 4.7等? 對於不同的目標處理器,x86,arm,mips等或x86的不同風格,如果這是你使用的本機編譯器,32位vs 64位等? 然后llvm(clang)針對不同的目標?
神秘在完成分析/優化代碼問題所需的思考過程中做得非常出色,期望編譯器能夠提出任何一個,而不是任何“現代編譯器”。
沒有進入數學屬性,這種形式的代碼
if (exponent < 0) {
r = mantissa << -exponent; /* diff */
} else {
r = mantissa >> exponent; /* diff */
}
return (r ^ -sign) + sign; /* diff */
將引導編譯器到A:以該形式實現它,執行if-then-else然后收斂於公共代碼以完成並返回。 或者B:保存分支,因為這是函數的尾部。 也不用擔心使用或保存r。
if (exponent < 0) {
return((mantissa << -exponent)^-sign)+sign;
} else {
return((mantissa << -exponent)^-sign)+sign;
}
然后你可以進入,因為Mystical指出sign符號變量一起消失所有代碼所寫的。 我不希望編譯器看到符號變量消失所以你應該自己做,而不是強迫編譯器試圖找出它。
這是深入了解gcc源代碼的絕佳機會。 看來你已經找到了一個案例,優化器在一個案例中看到了一件事,在另一個案例中看到了另一件事。 然后采取下一步,看看你是否不能讓gcc看到這種情況。 每個優化都在那里,因為一些個人或團體認可了優化並故意將其放在那里。 每次有人必須將它放在那里(然后測試它,然后將其保存到將來),這種優化就可以在那里工作。
絕對不要假設代碼越少越快,代碼越慢,就越容易創建並找到不正確的示例。 通常情況下,更少的代碼比更多的代碼更快。 正如我從一開始就證明的那樣,你可以創建更多的代碼來保存分支或者循環等,並且最終的結果是更快的代碼。
底線是您為編譯器提供了不同的源,並期望得到相同的結果。 問題不在於編譯器輸出,而在於用戶的期望。 對於特定的編譯器和處理器來說,相當容易演示,添加一行代碼會使整個函數顯着變慢。 例如,為什么改變a = b + 2; 到a = b + c + 2; 原因_fill_in_the_blank_compiler_name_生成完全不同且速度較慢的代碼? 答案當然是編譯器在輸入上輸入了不同的代碼,因此編譯器生成不同的輸出是完全有效的。 (更好的是當您交換兩條不相關的代碼行並導致輸出發生顯着變化時)輸入的復雜性和大小與輸出的復雜性和大小之間沒有預期的關系。 把這樣的東西喂成clang:
for(ra=0;ra<20;ra++) dummy(ra);
它產生了60-100行匯編程序。 它展開了循環。 我沒有計算行,如果你考慮它,它必須添加,將結果復制到函數調用的輸入,進行函數調用,最少三個操作。 所以取決於至少60個指令的目標,如果每個循環四個,則為80,如果每個循環為5,則為100,等等。
Mysticial已經給出了一個很好的解釋,但是我想我會補充說,為什么編譯器會為一個編譯器而不是另一個編譯器進行優化,實際上沒有什么根本的。
例如,LLVM的clang
編譯器為兩個函數提供相同的代碼(函數名除外),給出:
_fast_trunc_two: ## @fast_trunc_one
movl %edi, %edx
andl $-2147483648, %edx ## imm = 0xFFFFFFFF80000000
movl %edi, %esi
andl $8388607, %esi ## imm = 0x7FFFFF
orl $8388608, %esi ## imm = 0x800000
shrl $23, %edi
movzbl %dil, %eax
movl $150, %ecx
subl %eax, %ecx
js LBB0_1
shrl %cl, %esi
jmp LBB0_3
LBB0_1: ## %if.then
negl %ecx
shll %cl, %esi
LBB0_3: ## %if.end
movl %edx, %eax
negl %eax
xorl %esi, %eax
addl %edx, %eax
ret
此代碼不像OP中的第一個gcc版本那么短,但不像第二個那樣長。
來自另一個編譯器(我不會命名)的代碼,為x86_64編譯,為這兩個函數生成:
fast_trunc_one:
movl %edi, %ecx
shrl $23, %ecx
movl %edi, %eax
movzbl %cl, %edx
andl $8388607, %eax
negl %edx
orl $8388608, %eax
addl $150, %edx
movl %eax, %esi
movl %edx, %ecx
andl $-2147483648, %edi
negl %ecx
movl %edi, %r8d
shll %cl, %esi
negl %r8d
movl %edx, %ecx
shrl %cl, %eax
testl %edx, %edx
cmovl %esi, %eax
xorl %r8d, %eax
addl %edi, %eax
ret
這很有趣,因為它計算if
兩邊然后在最后使用條件移動來選擇正確的一個。
Open64編譯器生成以下內容:
fast_trunc_one:
movl %edi,%r9d
sarl $23,%r9d
movzbl %r9b,%r9d
addl $-150,%r9d
movl %edi,%eax
movl %r9d,%r8d
andl $8388607,%eax
negl %r8d
orl $8388608,%eax
testl %r8d,%r8d
jl .LBB2_fast_trunc_one
movl %r8d,%ecx
movl %eax,%edx
sarl %cl,%edx
.Lt_0_1538:
andl $-2147483648,%edi
movl %edi,%eax
negl %eax
xorl %edx,%eax
addl %edi,%eax
ret
.p2align 5,,31
.LBB2_fast_trunc_one:
movl %r9d,%ecx
movl %eax,%edx
shll %cl,%edx
jmp .Lt_0_1538
和fast_trunc_two
相似但不相同的代碼。
無論如何,當涉及到優化時,它是一個樂透 - 它就是它......它並不總是很容易知道為什么你的代碼以任何特定方式編譯。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.