編譯器能否對如下場景優化,以及如何檢查不同編譯器對此是否做了優化?
場景一
場景二
if (ival &> 0xffffffff - sizeof(some_type)) {...} else {...}
編譯器都會把場景一大於號右邊的部分優化為常量;
if (ival + sizeof(some_type) &> 0xffffffff) {...} else {...}
問題是對場景二編譯器也能作出優化嗎?
如何檢查不同編譯器對場景二是否做了優化?謝謝大家這麼用心回答我的問題,尤其感謝@左方園提供這麼詳細的數據!你們都太牛了,為此我想把引發這個問題的原場景貼出來,或許還能作更具體的分析。問題來源於nginx處理mp4模塊mdat盒子的一個bug,簡化一下那個調用如下
static size_t func(int64_t start, int64_t end)
{
int64_t dsize = end - start; /* end不小於start, 且都大於0 *//* 場景一 */
if ((uint64_t) dsize &> (uint64_t) 0xffffffff - sizeof(int64_t)) { ... } else { ... }/* 場景二 */
if ((uint64_t) dsize + sizeof(int64_t) &> (uint64_t) 0xffffffff) { ... } else { ... }...
}
不同編譯器有沒有對題主列舉的情況做優化,主要手段還是看最終生成的代碼。也就是 @Milo Yip 以及其他幾位大大說的「看彙編」。
如果對某些編譯器比較了解的話,其實看優化過程中編譯器IR比看彙編要更直觀一些,可以獲得更多語義信息。話說,題主的問題描述其實暗藏了許多坑呢。其實第二個情況有可能比第一個情況更容易優化…
前面有幾位大大的回答提到優化前後的語義不同了。原因是固定寬度的整數運算的溢出問題。
但對我來說問題描述中的代碼最坑的是signed與unsigned的交互問題,如果不是整天看這種代碼的同學的話可能不一定會立馬看出來——對熟手工來說當然是很明顯的事情。首先看題主的第一個例子:if (ival &> 0xFFFFFFFF - sizeof(some_type)) {...} else {...}
這個代碼的語法看起來是C或者C++。其中 sizeof() 運算符在C或C++中既可能是編譯時常量表達式,也可能是帶有運行時求值語義的表達式。假定這裡some_type是個簡單的固定大小的類型,而不是諸如VLA之類的會涉及運行時求值語義的類型。
繼續看,0xFFFFFFFF 的類型是什麼?Clang告訴我它是unsigned int。sizeof() 運算符的值是size_t類型的,在我的Mac OS X + 64-bit Clang環境(LP64)中它是 unsigned long,64位;在32位環境(ILP32)中size_t類型則是32位的。所以 0xFFFFFFFF - sizeof(some_type) 表達式的整體類型是將左操作數提升了類型之後的 unsigned long。`-BinaryOperator 0x104903408 &
|-ImplicitCastExpr 0x1049033f0 &
| `-IntegerLiteral 0x1049033a0 &
`-BinaryOperator 0x103084688 &
|-ImplicitCastExpr 0x103084670 &
| `-ImplicitCastExpr 0x103084658 &
| `-DeclRefExpr 0x1030845a0 &
`-BinaryOperator 0x103084630 &
|-ImplicitCastExpr 0x103084618 &
| `-IntegerLiteral 0x1030845c8 &
`-UnaryExprOrTypeTraitExpr 0x1030845f8 &
也就是說我們在這裡做的都是unsigned系的算術運算和比較。
C和C++的unsigned系整數類型都是帶有取模(modulo 2^n,其中n是對應整數類型的位數)語義的。這是well-defined的規定而不是undefiend behavior。於是我們再來看題主給的第二個例子:if (ival + sizeof(some_type) &> 0xFFFFFFFF) {...} else {...}
這裡把一個size_t類型的常量值放到了比較符號的左邊。而在我的測試環境中, ival + sizeof(some_type) 表達式的類型還是提升後的 unsigned long:
`-BinaryOperator 0x103007cd0 &
|-BinaryOperator 0x103007c70 &
| |-ImplicitCastExpr 0x103007c58 &
| | `-ImplicitCastExpr 0x103007c40 &
| | `-DeclRefExpr 0x103007bd8 &
| `-UnaryExprOrTypeTraitExpr 0x103007c20 &
`-ImplicitCastExpr 0x103007cb8 &
`-IntegerLiteral 0x103007c98 &
所以這裡同樣算術運算和比較都是unsigned系的,帶有取模語義。
自然,如果這個加法發生了算術溢出,那麼按照取模語義它的結果就會跟第一種代碼不一樣。所以不能隨意把第一種情況等價於第二種。
-------------------------
但是!前面只說了64為(LP64模型)的情況。如果不是64位,而是32位環境(ILP32模型)的情況呢?此時,0xFFFFFFFF還是unsigned int類型的整數字面量,而 sizeof() 運算符的值類型 size_t 也是32位的unsigned整數類型了。0xFFFFFFFF是個特殊的值——它是UINT32_MAX,所有unsigned int值中它就是最大的了。所以如果有int或者unsigned int的ival,然後我們來一個:
ival &> 0xFFFFFFFF
那麼這個表達式就可以直接被常量摺疊為false——不可能有32位unsigned整數類型可以比0xFFFFFFFF更大。
如果是 int ival 加上一個sizeof()呢?ival + sizeof(some_type) &> 0xFFFFFFFF
兩個32位unsigned整數之和,仍然不可能大於0xFFFFFFFF,所以這個表達式仍然可以被常量摺疊為false。
隨便找個近期一點的GCC或者Clang之類都可以看到這樣的優化。例如說int foo(int ival) {
return ival + sizeof(int) &> 0xFFFFFFFF;
}
用GCC 4.6.4在-O2級別編譯得到:
foo:
xorl %eax, %eax
ret
完全摺疊為false了。
拓展:另一個要警惕的字面量是0xFFFFFFFE。假如有這樣的表達式:ival &> 0xFFFFFFFE
許多編譯器做優化都會對這種邊界情況做特殊處理,將其改寫為:
ival == 0xFFFFFFFF
於是就又變成一個跟UINT32_MAX相比較的問題了。如果例子是:
ival + sizeof(int) &> 0xFFFFFFFE
則它會被改寫為:
ival == (0xFFFFFFFF - sizeof(int))
所以用0xFFFFFFFE字面量來「驗證編譯器是否做某些優化」的時候,請留意您要驗證的是否就是針對這樣邊界情況的優化,否則可能會誤判編譯器所做的事情喔。
還是用GCC 4.6.4 -O2來舉例:int foo(int ival) {
return ival + sizeof(int) &> 0xFFFFFFFE;
}
編譯得到:
foo:
xorl %eax, %eax
cmpl $-5, 4(%esp)
sete %al
ret
===========================
上面所討論的都是假定編譯器不知道 ival 變數的值域的情況(或者說只能假定bottom type的情況,也就是要假定可能可以取其類型的整個值域的任意一值)。
那麼如果編譯器可以知道 ival 變數的更具體的值域狀況——也就可以推導出會不會發生算術溢出——又如何呢?看例子:int foo(int ival) {
if (ival &> 0) {
if (ival + sizeof(int) &> 0xFFFFFFE0) {
return 42;
} else {
return 256;
}
} else {
return 17;
}
}
假如在32位環境(ILP32模型)下優化編譯這段代碼,(size_t類型是32位的unsigned整數),編譯器就可以通過外層的if判斷出 ival 變數的類型,然後在具體分支下使用這個更細緻的值域信息來進一步做優化。
例如說,這個函數用GCC 4.6.4在Linux上以32位模式來編譯,啟用-O2或者-O3級別優化,會看到:foo:
movl 4(%esp), %edx
movl $17, %eax
testl %edx, %edx
movl $256, %edx
cmovg %edx, %eax
ret
也就是說內層的 if (ival + sizeof(int) &> 0xFFFFFFE0) 被完全常量摺疊為false了。
稍微換一個例子,GCC 4.8.1在32位模式下-O2級別優化編譯:int foo(int ival) {
if ((ival 0x7FFFFFFF) == 0) {
if (ival + sizeof(int) &> 0xFFFFFFE0) {
return 42;
} else {
return 256;
}
} else {
return 17;
}
}
得到:
foo:
movl 4(%esp), %eax
andl $2147483647, %eax
cmpl $1, %eax
sbbl %eax, %eax
andl $239, %eax
addl $17, %eax
ret
同樣把內層的if摺疊掉為false了。
這種例子算是「作弊」么?從實用的角度說,顯然不算。
編譯器優化就是靠抽絲剝繭,一點點發掘出程序中的值的細緻性質,並且應用相應的優化。很多時候我們手寫的代碼中都會有冗餘,而編譯器在做優化時會儘可能看穿抽象層來分析程序中每個值的性質並且消除掉冗餘。如果上面的版本有些同學會感到不服氣,那請看下面的幾個變種。下面的版本里,foo()不再接收 ival 作為參數,而是把 ival 聲明為一個可見性限定在當前編譯單元中的全局變數:static int ival = 5;
int foo() {
if (ival + sizeof(int) &> 0xFFFFFFE0) {
return 42;
} else {
return 256;
}
}
假定這塊代碼全部在同一個.c源碼文件中,然後交給GCC 6.2.0以32位模式來編譯,看看-O2優化級別下我們會得到什麼?
foo:
movl $256, %eax
ret
嗯…直接知道 ival 肯定是5了,先把左邊給常量摺疊了。
假如這個源碼文件中還有別的函數可以對 ival 變數寫入值,那麼現有的不少C/C++編譯器都還沒實現正確細化其值域以達到預期優化的效果。例如說static int ival = 5;
int foo() {
if (ival + sizeof(int) &> 0xFFFFFFE0) {
return 42;
} else {
return 256;
}
}
void bar(int n) {
if (n &> 0) {
ival = n;
}
}
其實做充分的過程間分析(interprocedural analysis)的話是可以判定 ival 的值域肯定在 (0, INT32_MAX] 範圍內,因而還是可以對 foo() 里的判斷做常量摺疊——但近期的GCC、Clang、ICC即便打開了IPA / IPO也還沒有實現這個優化。
不過如果這個源碼文件通過LTO編譯優化,在鏈接時發現整個程序里沒有任何別的地方調用過 bar() 的話,就可以在LTO時把bar()刪除掉,於是就又可以把 foo() 里的判斷常量摺疊了。
===========================
至於題主補充的nginx mdat模塊的代碼的場景二:if ((uint64_t) dsize + sizeof(int64_t) &> (uint64_t) 0xffffffff) { ... } else { ... }
單就題主給出的代碼而言,這裡做不了什麼優化了。dsize的細化值域信息未知(只能用int64_t的完整值域),與常量8相加可能溢出,而右邊的0xffffffff在擴展後也不是什麼特殊邊界值,所以只能乖乖加了再比較。
如果說這個func()函數所在的源碼文件里,調用func()的地方有什麼特別之處的話,那這個上下文信息倒是有可能可以進一步用於優化的。留意到func()是「static」的,意味著它只能對所在的編譯單元中的代碼可見,應用上IPA / IPO或許能得到點更有趣的上下文信息。
在Clang 3.9.0、GCC 6.2.0和ICC 17 64位(LP64模型)用-O3編譯實驗了一下下面的代碼:#include &
static int foo(int64_t end, int64_t start) {
int64_t d = end - start;
return d + sizeof(int64_t) &> (uint64_t) 0xFFFFFFFF;
}
int bar(int64_t end, int64_t start) {
if (end &> start) return foo(end, start);
else return 0;
}
結果都差不多,比較左邊的常量並沒有被移動到右邊。
加入了一些新的測試用例 2016-11-22 20:30
先上結論,場景1會優化,也就是只做一次比較,場景2不會,會做一次加法再比較。
原因並不是編譯器不能做這個優化,而是場景2在數學上看起來和情況1等價,但實際運行中並不是。在場景2中,因為編譯器並不知道ival具體的值,與sizeof(T)相加可能會溢出,如果移到右邊變成了減去sizeof(T),則不會溢出了,這就與未優化的版本的行為不一致了。
這種優化在CSAPP第五章中有提到,叫做不安全優化。
但在 @諸葛不亮 提到的情況下,避免了溢出的問題,依舊不會移到右邊,至於原因,答主也不了解,希望知友不吝賜教。(相關彙編代碼在最後面)
然後 給@Milo Yip 補上彙編代碼
首先是C++源代碼
#include&
template&
void test_optimization(unsigned ival)
{
if (ival &> vlaue - sizeof(T))
std::cout &<&< ival &<&< " &> " &<&< vlaue &<&< " - " &<&< sizeof(T) &<&< std::endl;
else
std::cout &<&< ival &<&< " &<= " &<&< vlaue &<&< " - " &<&< sizeof(T) &<&< std::endl;
if (ival + sizeof(T) &> vlaue)
std::cout &<&< ival &<&< " + " &<&< sizeof(T) &<&< " &> " &<&< vlaue &<&< std::endl;
else
std::cout &<&< ival &<&< " + " &<&< sizeof(T) &<&< " &<= " &<&< vlaue &<&< std::endl;
}
int main()
{
test_optimization&
test_optimization&
return 0;
}
下面看看上面的代碼在不同環境下的優化情況
首先是:windows10 ,vs 2013 , 32 位,debug模式(下面標出了源代碼和彙編代碼的對應,還顯示了變數地址替換成了變數名,下同):
主函數:
int main()
{
009B6430 push ebp
009B6431 mov ebp,esp
009B6433 sub esp,0C0h
009B6439 push ebx
009B643A push esi
009B643B push edi
009B643C lea edi,[ebp-0C0h]
009B6442 mov ecx,30h
009B6447 mov eax,0CCCCCCCCh
009B644C rep stos dword ptr es:[edi]
test_optimization&
009B644E push 0Ah
009B6450 call test_optimization&
009B6455 add esp,4
test_optimization&
009B6458 push 2
009B645A call test_optimization&
009B645F add esp,4
return 0;
009B6462 xor eax,eax
}
注意到上面的這兩個函數調用:
009B6450 call test_optimization&
009B645A call test_optimization& 這兩個函數的彙編代碼如下: test_optimization& 可以看到,對於if (ival &> vlaue - sizeof(T)): 編譯器直接計算出了12 - sizeof(int)的結果8,然後與ival比較,基本已經優化的很好了 對應的double版本的也類似,只是比較的數是4(12-8),加上的數是8,彙編代碼如下: test_optimization&
void test_optimization(unsigned ival)
{
01113470 push ebp
01113471 mov ebp,esp
01113473 sub esp,0C4h
01113479 push ebx
0111347A push esi
0111347B push edi
0111347C lea edi,[ebp-0C4h]
01113482 mov ecx,31h
01113487 mov eax,0CCCCCCCCh
0111348C rep stos dword ptr es:[edi]
if (ival &> vlaue - sizeof(T))
0111348E cmp dword ptr [ival],8
01113492 jbe test_optimization&
std::cout &<&< ival &<&< " &> " &<&< vlaue &<&< " - " &<&< sizeof(T) &<&< std::endl;
中間略去部分輸出和分支彙編代碼
......
......
......
if (ival + sizeof(T) &> vlaue)
01113590 mov eax,dword ptr [ival]
01113593 add eax,4
01113596 cmp eax,0Ch
01113599 jbe test_optimization&
std::cout &<&< ival &<&< " + " &<&< sizeof(T) &<&< " &> " &<&< vlaue &<&< std::endl;
0111348E cmp dword ptr [ival],8
對於if (ival + sizeof(T) &> vlaue):
編譯器先把ival 加上了4,然後與0ch(頁就是12)比較,這裡多做了一次加法,「看起來最好的情況」應該和if (ival &> vlaue - sizeof(T))產生一樣的彙編代碼(注意引號)void test_optimization(unsigned ival)
{
01113740 push ebp
01113741 mov ebp,esp
01113743 sub esp,0C4h
01113749 push ebx
0111374A push esi
0111374B push edi
0111374C lea edi,[ebp-0C4h]
01113752 mov ecx,31h
01113757 mov eax,0CCCCCCCCh
0111375C rep stos dword ptr es:[edi]
if (ival &> vlaue - sizeof(T))
0111375E cmp dword ptr [ival],4
01113762 jbe test_optimization&
std::cout &<&< ival &<&< " &> " &<&< vlaue &<&< " - " &<&< sizeof(T) &<&< std::endl;
中間略去部分輸出和分支彙編代碼
......
......
......
if (ival + sizeof(T) &> vlaue)
01113860 mov eax,dword ptr [ival]
01113863 add eax,8
01113866 cmp eax,0Ch
01113869 jbe test_optimization&
std::cout &<&< ival &<&< " + " &<&< sizeof(T) &<&< " &> " &<&< vlaue &<&< std::endl;
其次是:windows10 ,vs 2013 , 32 位,release模式
主函數:
0037129C int 3
0037129D int 3
0037129E int 3
0037129F int 3
--- main.cpp
test_optimization&
003712A0 call test_optimization&
test_optimization&
003712A5 call test_optimization&
return 0;
003712AA xor eax,eax
注意到上面的這兩個函數調用:
003712A0 call test_optimization&
003712A5 call test_optimization& 發生了什麼,編譯器直接call兩個函數了,連參數都沒有壓棧啊!!! 去看看這兩個函數: test_optimization& 中間略去部分輸出和分支彙編代碼 if (ival + sizeof(T) &> vlaue) 對比上面的注釋,可以看到: 對應的double版本的也類似,就不附上彙編代碼了。 下面我們把源代碼改成這樣,不再用常量調用了,改為從控制台輸入,代碼如下: template& 下面還是:windows10 ,vs 2013 , 32 位,release模式的彙編代碼,輸入為 10 2 主函數: 先看下 調用的函數: 中間略去部分輸出和分支彙編代碼 if (ival + sizeof(T) &> vlaue) 對於if (ival &> vlaue - sizeof(T)):
void test_optimization(unsigned ival)
{
00371740 push ebp
00371741 mov ebp,esp
00371743 and esp,0FFFFFFF8h
if (ival &> vlaue - sizeof(T))
std::cout &<&< ival &<&< " &> " &<&< vlaue &<&< " - " &<&< sizeof(T) &<&< std::endl;
00371746 push 371AC0h
0037174B push 4 // 把4壓棧
0037174D push 0Ch // 把12壓棧
0037174F push ecx
00371750 mov ecx,dword ptr ds:[373058h]
00371756 push 0Ah // 把10壓棧
00371758 call dword ptr ds:[373028h] //輸出10
0037175E mov edx,3731E8h
00371763 mov ecx,eax
00371765 call std::operator&<&<&
0037176A add esp,4
if (ival &> vlaue - sizeof(T))
std::cout &<&< ival &<&< " &> " &<&< vlaue &<&< " - " &<&< sizeof(T) &<&< std::endl;
0037176D mov ecx,eax
0037176F call dword ptr ds:[373028h] //輸出 12
00371775 mov edx,3731E4h
0037177A mov ecx,eax
0037177C call std::operator&<&<&
00371781 mov ecx,eax
00371783 call dword ptr ds:[373028h] //輸出4
00371789 mov ecx,eax
0037178B call dword ptr ds:[37302Ch] //輸出換行
......
......
......
std::cout &<&< ival &<&< " + " &<&< sizeof(T) &<&< " &> " &<&< vlaue &<&< std::endl;
00371791 push 371AC0h
00371796 push 0Ch
00371798 push 4
0037179A push ecx
0037179B mov ecx,dword ptr ds:[373058h] //輸出10
003717A1 push 0Ah
003717A3 call dword ptr ds:[373028h]
003717A9 mov edx,3731F4h
003717AE mov ecx,eax
003717B0 call std::operator&<&<&
003717B5 add esp,4
003717B8 mov ecx,eax
003717BA call dword ptr ds:[373028h] //輸出4
003717C0 mov edx,3731E8h
003717C5 mov ecx,eax
003717C7 call std::operator&<&<&
003717CC mov ecx,eax
003717CE call dword ptr ds:[373028h] //輸出12
003717D4 mov ecx,eax
003717D6 call dword ptr ds:[37302Ch] //輸出換行
兩個if,編譯器連比較都沒做,就直接輸出了,簡直太兇殘。
原因就是我們調用函數時,直接給的常量10和2,編譯器當然可以優化掉參數,直接輸出了#include&
void test_optimization(unsigned ival)
{
if (ival &> vlaue - sizeof(T))
std::cout &<&< ival &<&< " &> " &<&< vlaue &<&< " - " &<&< sizeof(T) &<&< std::endl;
else
std::cout &<&< ival &<&< " &<= " &<&< vlaue &<&< " - " &<&< sizeof(T) &<&< std::endl;
if (ival + sizeof(T) &> vlaue)
std::cout &<&< ival &<&< " + " &<&< sizeof(T) &<&< " &> " &<&< vlaue &<&< std::endl;
else
std::cout &<&< ival &<&< " + " &<&< sizeof(T) &<&< " &<= " &<&< vlaue &<&< std::endl;
}
int main()
{
unsigned ival1;
std::cin &>&> ival1;
test_optimization&
unsigned ival2;
std::cin &>&> ival2;
test_optimization&
return 0;
}
int main()
{
010312A0 push ebp
010312A1 mov ebp,esp
010312A3 sub esp,0Ch
010312A6 mov eax,dword ptr ds:[01034000h]
010312AB xor eax,ebp
010312AD mov dword ptr [ebp-4],eax
unsigned ival1;
std::cin &>&> ival1;
010312B0 mov ecx,dword ptr ds:[103305Ch]
010312B6 lea eax,[ival1]
010312B9 push eax
010312BA call dword ptr ds:[1033044h]
test_optimization&
010312C0 mov ecx,dword ptr [ival1] //將輸入結果10存入ecx寄存器
010312C3 call test_optimization&
unsigned ival2;
std::cin &>&> ival2;
010312C8 mov ecx,dword ptr ds:[103305Ch]
010312CE lea eax,[ival2]
010312D1 push eax
010312D2 call dword ptr ds:[1033044h]
test_optimization&
010312D8 mov ecx,dword ptr [ival2]
010312DB call test_optimization&
return 0;
}
010312C3 call test_optimization&
void test_optimization(unsigned ival)
{
01031780 push ebp
01031781 mov ebp,esp
01031783 and esp,0FFFFFFF8h
01031786 push ecx
01031787 push esi
if (ival &> vlaue - sizeof(T))
std::cout &<&< ival &<&< " &> " &<&< vlaue &<&< " - " &<&< sizeof(T) &<&< std::endl;
01031788 push 1031B80h
0103178D push 4
0103178F push 0Ch
01031791 mov esi,ecx // 將ecx寄存器的值10存入esi寄存器
01031793 push ecx
01031794 mov ecx,dword ptr ds:[1033060h]
0103179A push esi
0103179B call dword ptr ds:[103302Ch]
010317A1 mov ecx,eax
010317A3 mov edx,10331E8h
010317A8 cmp esi,8 // 將esi寄存器的值10與8比較
010317AB ja test_optimization&
......
......
......
std::cout &<&< ival &<&< " + " &<&< sizeof(T) &<&< " &> " &<&< vlaue &<&< std::endl;
010317DE push 1031B80h
010317E3 push 0Ch
010317E5 push 4
010317E7 lea eax,[esi+4] //將esi加上4存到eax,eax現在是14
010317EA push ecx
010317EB mov ecx,dword ptr ds:[1033060h]
010317F1 push esi
010317F2 cmp eax,0Ch //將eax(值為14)與12比較
010317F5 jbe test_optimization&
010317F7 call dword ptr ds:[103302Ch]
和上面debug模式一樣,編譯器直接計算出了12 - sizeof(int)的結果8,然後與ival比較
對於if (ival &> vlaue - sizeof(T)):
也是做了一次加法,然後比較
再是:Ubuntu 16.04.1 ,GCC 5.4.0 ,:
O1優化(代碼同輸入版本):
看下彙編代碼
主函數:
0000000000400a86 &
400a86: 48 83 ec 18 sub $0x18,%rsp
400a8a: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
400a91: 00 00
400a93: 48 89 44 24 08 mov %rax,0x8(%rsp)
400a98: 31 c0 xor %eax,%eax
400a9a: 48 89 e6 mov %rsp,%rsi
400a9d: bf a0 20 60 00 mov $0x6020a0,%edi
400aa2: e8 29 fe ff ff callq 4008d0 &<_ZNSi10_M_extractIjEERSiRT_@plt&>
400aa7: 8b 3c 24 mov (%rsp),%edi
400aaa: e8 5e 00 00 00 callq 400b0d &<_Z17test_optimizationIiLj12EEvj&>
400aaf: 48 8d 74 24 04 lea 0x4(%rsp),%rsi
400ab4: bf a0 20 60 00 mov $0x6020a0,%edi
400ab9: e8 12 fe ff ff callq 4008d0 &<_ZNSi10_M_extractIjEERSiRT_@plt&>
400abe: 8b 7c 24 04 mov 0x4(%rsp),%edi
400ac2: e8 f6 02 00 00 callq 400dbd &<_Z17test_optimizationIdLj12EEvj&>
400ac7: 48 8b 54 24 08 mov 0x8(%rsp),%rdx
400acc: 64 48 33 14 25 28 00 xor %fs:0x28,%rdx
400ad3: 00 00
400ad5: 74 05 je 400adc &
400ad7: e8 74 fe ff ff callq 400950 &<__stack_chk_fail@plt&>
400adc: b8 00 00 00 00 mov $0x0,%eax
400ae1: 48 83 c4 18 add $0x18,%rsp
400ae5: c3 retq
看下
400aaa: e8 5e 00 00 00 callq 400b0d &<_Z17test_optimizationIiLj12EEvj&>
調用的函數:
0000000000400b0d &<_Z17test_optimizationIiLj12EEvj&>:
400b0d: 41 54 push %r12
400b0f: 55 push %rbp
400b10: 53 push %rbx
400b11: 89 fb mov %edi,%ebx
400b13: 83 ff 08 cmp $0x8,%edi //直接與8比較
400b16: 0f 86 a7 00 00 00 jbe 400bc3 &<_Z17test_optimizationIiLj12EEvj+0xb6&>
400b1c: 89 fe mov %edi,%esi
400b1e: bf c0 21 60 00 mov $0x6021c0,%edi
400b23: e8 38 fe ff ff callq 400960 &<_ZNSo9_M_insertImEERSoT_@plt&>
中間略去部分輸出和分支彙編代碼
......
......
......
400c67: 48 8d 46 04 lea 0x4(%rsi),%rax //加上 4
400c6b: 48 83 f8 0c cmp $0xc,%rax //與12比較
400c6f: 0f 86 a4 00 00 00 jbe 400d19 &<_Z17test_optimizationIiLj12EEvj+0x20c&>
和上面一樣做了一次加法
O2優化(代碼同輸入版本):
看下彙編代碼
主函數:
00000000004009d0 &
4009d0: 48 83 ec 18 sub $0x18,%rsp
4009d4: bf a0 20 60 00 mov $0x6020a0,%edi
4009d9: 48 89 e6 mov %rsp,%rsi
4009dc: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
4009e3: 00 00
4009e5: 48 89 44 24 08 mov %rax,0x8(%rsp)
4009ea: 31 c0 xor %eax,%eax
4009ec: e8 1f ff ff ff callq 400910 &<_ZNSi10_M_extractIjEERSiRT_@plt&>
4009f1: 8b 3c 24 mov (%rsp),%edi
4009f4: e8 77 01 00 00 callq 400b70 &<_Z17test_optimizationIiLj12EEvj&>
4009f9: 48 8d 74 24 04 lea 0x4(%rsp),%rsi
4009fe: bf a0 20 60 00 mov $0x6020a0,%edi
400a03: e8 08 ff ff ff callq 400910 &<_ZNSi10_M_extractIjEERSiRT_@plt&>
400a08: 8b 7c 24 04 mov 0x4(%rsp),%edi
400a0c: e8 5f 03 00 00 callq 400d70 &<_Z17test_optimizationIdLj12EEvj&>
400a11: 48 8b 54 24 08 mov 0x8(%rsp),%rdx
看下
4009f4: e8 77 01 00 00 callq 400b70 &<_Z17test_optimizationIiLj12EEvj&>
調用的函數:
0000000000400b70 &<_Z17test_optimizationIiLj12EEvj&>:
400b70: 41 54 push %r12
400b72: 83 ff 08 cmp $0x8,%edi //與8比較
400b75: 55 push %rbp
400b76: 53 push %rbx
400b77: 89 fb mov %edi,%ebx
400b79: bf c0 21 60 00 mov $0x6021c0,%edi
400b7e: 48 89 de mov %rbx,%rsi
400b81: 0f 86 21 01 00 00 jbe 400ca8 &<_Z17test_optimizationIiLj12EEvj+0x138&> //&<=則跳轉
400b87: e8 14 fe ff ff callq 4009a0 &<_ZNSo9_M_insertImEERSoT_@plt&>
中間略去部分輸出和分支彙編代碼
......
......
......
400c09: 48 8d 43 04 lea 0x4(%rbx),%rax //加上 4
400c0d: 48 89 de mov %rbx,%rsi
400c10: bf c0 21 60 00 mov $0x6021c0,%edi
400c15: 48 83 f8 0c cmp $0xc,%rax //與12比較
400c19: 0f 87 a1 00 00 00 ja 400cc0 &<_Z17test_optimizationIiLj12EEvj+0x150&> // &> 則跳轉
和上面一樣做了一次加法
謝 @諸葛不亮 的提醒,如果把if里寫成uint64_t(ival),而ival本身是uint_32,轉換後加上size_t理也不會溢出了(32位)。那麼編譯器會不會把加法優化掉呢?
把測試代碼更改如下(為方便觀察,輸入輸出全部都是16進位格式):
#include&
template& windows10 ,vs 2015 , 32 位,debug模式: 運行結果: 0xa + 0x4 &> 0xc 0xa + 0x4 &> 0xc 0x2 &<= 0xffffffff - 0x4
0x2 + 0x4 &<= 0xffffffff
0x2 + 0x4 &<= 0xffffffff
0xffffffff &> 0xa - 0x8 0xffffffff + 0x8 &<= 0xa
0xffffffff + 0x8 &> 0xa 下面直接看,第一個版本的函數: test_optimization&
場景一二和前面分析的一樣。
void test_optimization(uint32_t ival)
{
/* 場景一 */
if (ival &> vlaue - sizeof(T))
std::cout &<&< "0x"&<&< ival &<&< " &> 0x" &<&< vlaue &<&< " - 0x" &<&< sizeof(T) &<&< std::endl;
else
std::cout &<&< "0x" &<&< ival &<&< " &<= 0x" &<&< vlaue &<&< " - 0x" &<&< sizeof(T) &<&< std::endl;
std::cout &<&< std::endl;
/* 場景二 */
if (ival + sizeof(T) &> vlaue)
std::cout &<&< "0x" &<&< ival &<&< " + 0x" &<&< sizeof(T) &<&< " &> 0x" &<&< vlaue &<&< std::endl;
else
std::cout &<&< "0x" &<&< ival &<&< " + 0x" &<&< sizeof(T) &<&< " &<= 0x" &<&< vlaue &<&< std::endl;
std::cout &<&< std::endl;
/* 場景三 @諸葛不亮 提到的*/
if (uint64_t(ival) + sizeof(T) &> vlaue)
std::cout &<&< "0x" &<&< ival &<&< " + 0x" &<&< sizeof(T) &<&< " &> 0x" &<&< vlaue &<&< std::endl;
else
std::cout &<&< "0x" &<&< ival &<&< " + 0x" &<&< sizeof(T) &<&< " &<= 0x" &<&< vlaue &<&< std::endl;
std::cout &<&< std::endl;
}
int main()
{
const size_t size = 3;
uint32_t vals[size];
std::cin &>&> std::hex;
for (size_t i = 0; i &< size; i++)//輸入為 0xa 0x2 0xfffffff
std::cin &>&> vals[i];
std::cout &<&< std::hex;
test_optimization&
test_optimization&
return 0;
}
0xa 0x2 0xffffffff
0xa &> 0xc - 0x4
void test_optimization(uint32_t ival)
{
00DDB2F0 push ebp
00DDB2F1 mov ebp,esp
00DDB2F3 sub esp,0C8h
00DDB2F9 push ebx
00DDB2FA push esi
00DDB2FB push edi
00DDB2FC lea edi,[ebp-0C8h]
00DDB302 mov ecx,32h
00DDB307 mov eax,0CCCCCCCCh
00DDB30C rep stos dword ptr es:[edi]
/* 場景一 */
if (ival &> vlaue - sizeof(T))
00DDB30E cmp dword ptr [ival],8 //依舊直接與8比較
00DDB312 jbe test_optimization&
std::cout &<&< "0x"&<&< ival &<&< " &> 0x" &<&< vlaue &<&< " - 0x" &<&< sizeof(T) &<&< std::endl;
/* 場景二 */
if (ival + sizeof(T) &> vlaue)
00DDB451 mov eax,dword ptr [ival]
00DDB454 add eax,4 //加4
00DDB457 cmp eax,0Ch //然後比較
00DDB45A jbe test_optimization&
std::cout &<&< "0x" &<&< ival &<&< " + 0x" &<&< sizeof(T) &<&< " &> 0x" &<&< vlaue &<&< std::endl;
/* 場景三 @諸葛不亮 提到的*/
if (uint64_t(ival) + sizeof(T) &> vlaue)
00DDB599 mov eax,dword ptr [ival] //ival變數存入eax
00DDB59C xor ecx,ecx //ecx置0
00DDB59E add eax,4 //加4
00DDB5A1 adc ecx,0 //如果有進位就存在ecx中
00DDB5A4 mov dword ptr [ebp-0C8h],eax //uint64_t低32位存儲
00DDB5AA mov dword ptr [ebp-0C4h],ecx //uint64_t高32位存儲,即剛剛的進位
00DDB5B0 cmp dword ptr [ebp-0C4h],0 //如果高位大於0,則直接跳轉到if分支
00DDB5B7 ja test_optimization&
00DDB5B9 cmp dword ptr [ebp-0C8h],0Ch //然後把低32位與12比
00DDB5C0 jbe test_optimization&
注意到場景三的彙編,轉換成64位後,做了兩個32位的比較,判斷過程是:先把ival與sizeof(T) 4加,結果在eax中,進位在ecx中,然後存到一個64位內存塊中,再做兩次比較,一次高位,依次低位。 總之,場景二三都沒有優化掉加法。
後面幾個版本也是和上面一樣,只是比較和加上的數字不一樣,就不附上彙編代碼了。
下面看看,release模式
windows10 ,vs 2015 , 32 位,release模式:
運行結果同debug模式
同樣的,也來看看第一個版本的函數的彙編代碼:
test_optimization&
void test_optimization(uint32_t ival)
{
00EB3410 push ebp
00EB3411 mov ebp,esp
00EB3413 and esp,0FFFFFFF8h
00EB3416 sub esp,0Ch
00EB3419 mov edx,offset string "0x" (0EB5444h)
...
...
...
/* 場景一 */
if (ival &> vlaue - sizeof(T))
std::cout &<&< "0x"&<&< ival &<&< " &> 0x" &<&< vlaue &<&< " - 0x" &<&< sizeof(T) &<&< std::endl;
00EB341E push esi
00EB341F push offset std::endl&
00EB3424 push 4
00EB3426 push 0Ch
00EB3428 push ecx
00EB3429 mov esi,ecx
00EB342B push esi
00EB342C push ecx
00EB342D mov ecx,dword ptr [_imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A (0EB50E4h)]
00EB3433 call std::operator&<&<&
00EB3438 add esp,4
00EB343B mov ecx,eax
00EB343D call dword ptr [__imp_std::basic_ostream&
00EB344A cmp esi,8 //依舊直接與8比較
00EB344D ja test_optimization&
std::cout &<&< "0x" &<&< ival &<&< " + 0x" &<&< sizeof(T) &<&< " &> 0x" &<&< vlaue &<&< std::endl;
00EB3491 push offset std::endl&
00EB3496 push 0Ch
00EB3498 push 4
00EB349A push ecx
00EB349B lea eax,[esi+4] //加4
00EB349E mov edx,offset string "0x" (0EB5444h)
00EB34A3 push esi
00EB34A4 push ecx
00EB34A5 mov ecx,dword ptr [_imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A (0EB50E4h)]
00EB34AB cmp eax,0Ch //然後比較
00EB34AE jbe test_optimization&
00EB34B0 call std::operator&<&<&
...
...
...
/* 場景三 @諸葛不亮 提到的*/
if (uint64_t(ival) + sizeof(T) &> vlaue)
00EB3532 mov ecx,esi //把esi的值10存到ecx中
00EB3534 xor eax,eax //eax置0,做ival的高32位
00EB3536 add ecx,4 //加4
00EB3539 adc eax,eax //如果有進位就存在eax中
00EB353B mov dword ptr [esp+0Ch],eax //存高位
00EB353F jne test_optimization&
00EB3541 cmp ecx,0Ch //然後把低32位與12比
00EB3544 ja test_optimization&
場景一二和前面分析的一樣。
注意到場景三的彙編,特別有意思,轉換成64位後,只做了32位的比較,優化掉了一次比較,判斷過程是:先把ival與sizeof(T) 4加,結果在ecx中,進位在eax中,有趣的是,高位沒有比較,利用add ecx,4這一句,如果進位了,那麼下一句adc eax,eax(帶進位加法),結果就是1,那麼zf標記位就為1,jne就跳轉了(大於),如果沒進位,那麼zf標記位就為0.那麼繼續比較一下低位。 總之,場景二三都沒有優化掉加法,但是release下編譯器把本來需要做兩次比較的64位整數大小比較優化成只比較一次,而且優化方式特別巧妙。
上面只是在32位上多做了加法,下面看看64位的彙編代碼:
直接上release模式吧!
windows10 ,vs2015,64位, release模式 test_optimization&
void test_optimization(uint32_t ival)
{
00007FF7E0BB1150 mov qword ptr [rsp+8],rbx
00007FF7E0BB1155 push rdi
00007FF7E0BB1156 sub rsp,20h
00007FF7E0BB115A mov ebx,ecx
/* 場景一 */
if (ival &> vlaue - sizeof(T))
std::cout &<&< "0x" &<&< ival &<&< " &> 0x" &<&< vlaue &<&< " - 0x" &<&< sizeof(T) &<&< std::endl;
00007FF7E0BB115C lea rdx,[string "0x" (07FF7E0BB32A0h)]
00007FF7E0BB1163 cmp ecx,8 //直接與8比較
00007FF7E0BB1166 mov rcx,qword ptr [__imp_std::cout (07FF7E0BB30F8h)]
00007FF7E0BB116D jbe test_optimization&
00007FF7E0BB116F call std::operator&<&<&
...
...
...
/* 場景二 */
if (ival + sizeof(T) &> vlaue)
std::cout &<&< "0x" &<&< ival &<&< " + 0x" &<&< sizeof(T) &<&< " &> 0x" &<&< vlaue &<&< std::endl;
00007FF7E0BB11F6 mov rcx,qword ptr [__imp_std::cout (07FF7E0BB30F8h)]
00007FF7E0BB11FD lea rdx,[string "0x" (07FF7E0BB32A0h)]
00007FF7E0BB1204 lea rdi,[rbx+4] //加4
00007FF7E0BB1208 call std::operator&<&<&
00007FF7E0BB120D mov rcx,rax
00007FF7E0BB1210 mov edx,ebx
00007FF7E0BB1212 call qword ptr [__imp_std::basic_ostream&
00007FF7E0BB1227 mov rcx,rax
00007FF7E0BB122A mov edx,4
00007FF7E0BB122F call qword ptr [__imp_std::basic_ostream&
00007FF7E0BB123C mov rcx,rax
00007FF7E0BB123F cmp rdi,0Ch //然後比較
00007FF7E0BB1243 ja test_optimization&
...
...
...
/* 場景三 @諸葛不亮 提到的*/
if (uint64_t(ival) + sizeof(T) &> vlaue)
std::cout &<&< "0x" &<&< ival &<&< " + 0x" &<&< sizeof(T) &<&< " &> 0x" &<&< vlaue &<&< std::endl;
00007FF7E0BB1283 mov rcx,qword ptr [__imp_std::cout (07FF7E0BB30F8h)]
00007FF7E0BB128A lea rdx,[string "0x" (07FF7E0BB32A0h)]
00007FF7E0BB1291 call std::operator&<&<&
00007FF7E0BB1296 mov rcx,rax
00007FF7E0BB1299 mov edx,ebx
00007FF7E0BB129B call qword ptr [__imp_std::basic_ostream&
00007FF7E0BB12B0 mov rcx,rax
00007FF7E0BB12B3 mov edx,4
00007FF7E0BB12B8 call qword ptr [__imp_std::basic_ostream&
00007FF7E0BB12C5 mov rcx,rax
00007FF7E0BB12C8 cmp rdi,0Ch //直接與12比較
00007FF7E0BB12CC ja test_optimization&
這應該是最精彩的地方了,場景一二和上面一樣, 場景三的的確確把加法優化掉了!!! 但不是我們想到的方式,而是利用函數執行到場景二的時候,已經把ival + sizeof(T)加過了,直接拿來比較就好了,另外,由於本來就是64位的寄存器(rdi,rcx等等),不用考錄進位的問題。 的確精彩
總結:
在最上面的四種環境下,情景2的sizeof(T)都沒有被優化到右邊,而是做了一次加法,因為在場景2中,因為編譯器並不知道ival具體的值,與sizeof(T)相加可能會溢出,如果移到右邊變成了減去sizeof(T),則不會溢出了,這就與未優化的版本的行為不一致了。
這種優化在CSAPP第五章中有提到,叫做不安全優化。
@諸葛不亮提到的我也測試了,編譯器沒有優化掉加法。 32位編譯選項中,在debug模式下,反倒是因為64的整數多做了一次比較,release模式巧妙的優化掉了一次。所以在自己寫代碼的是還是不要想當然的幫助編譯器做優化,這樣可能做可能會起到相反的作用。 64位編譯選項中,場景三的的確確把加法優化掉了一次,但並不是移到了右邊,而是利用之前已經做過加法的結果,優化掉了一次加法。
評論提到了一個重要的問題,我之前並沒有意識到,那就是補充的代碼是64位。這會影響到生成的代碼,然而目前從我測試的編譯器,即使是64位,Clang 4.0 SVN / GCC 6.2 O3均不會操作數比較,優化成減法。
下面是有關我之前處理32位的一些情況,作為參考:
ival不能在編譯期間確定,sizeof在編譯時期可以確定
1可以
if (ival &> 0xffffffff - sizeof(some_type)) {...} ==&> if (ival &> 0xffffffff - C1) ====&> if(ival &> C2)
2不能
if (ival + sizeof(some_type) &> 0xffffffff) {...} ==&> if(ival + C1 &> 0xffffffff)加法是不能隨便就變為減法的,如下面例子:
#include &
int m = 0;
int foo(int i)
{
if (i + sizeof(int) &> 0xffffff)
{
printf("Hi
");
}
else
{
printf("iH
");
}
return i;
}
int main()
{
foo(m);
}
使用clang -O3 -S -emit-llvm
; Function Attrs: nounwind ssp uwtable
define i32 @foo(i32 returned %i) local_unnamed_addr #0 {
entry:
%conv = sext i32 %i to i64
%add = add nsw i64 %conv, 4
%cmp = icmp ugt i64 %add, 16777215
br i1 %cmp, label %if.then, label %if.else
if.then: ; preds = %entry
%puts4 = tail call i32 @puts(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @str.2, i64 0, i64 0))
br label %if.end
if.else: ; preds = %entry
%puts = tail call i32 @puts(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @str, i64 0, i64 0))
br label %if.end
if.end: ; preds = %if.else, %if.then
ret i32 %i
}
這裡的add nsw告訴了原因,查看LLVM Language Reference Manual 可以找到這裡:
nuw and nsw stand for 「No Unsigned Wrap」 and 「No Signed Wrap」, respectively. If the nuw and/or nsw keywords are present, the result value of the add is a poison value if unsigned and/or signed overflow, respectively, occurs.所以你不能移到右邊去。對於1,若編譯器前端做的足夠好,完全可以在到達中間代碼之前就進行常量處理,不用交給後端去進行優化處理,尤其是很多編譯器都支持C++的constexpr了,做這個事情簡直不會吹灰之力。
要查看是否優化的話,Clang可以查看LLVM IR / 彙編,其餘的可以查看彙編。檢查的方法是看彙編。
我幫題主檢查了Chakra JIT生成的code,應該是沒有做 2 的優化。我其實有個疑問,如果做 2 的優化安全嗎,如果左邊的加法本來會對輸入變數產生溢出什麼的,把常量移到右邊變成減法,這個效果不就沒有了嗎?
第一個會用constant folding來進行優化直接算出來。第二個的話,對於大於號左邊的expression, 是不會constant folding的因為ival不是constant。我認為最終也不太會優化的原因是如果大於號左邊不止兩個operand, 難道編譯器還需要把裡面的constant都挑出來然後移到右邊嗎。。。這只是猜測。。。至於看到底有沒有優化的話方法就是之前有個提到的用-S -O[0-4]然後diff生成的彙編。
這種優化叫常量摺疊(constant folding),一般的編譯器都會做。
編譯器在生成抽象語法樹時並不會局限余「大於號左邊還是右邊」。比你你這個場景二,如果吧if語句里的boolean expression看作AST根節點,那麼編譯在優化AST階段,很有可能通過一些策略來"merge"左右節點中可以constant folding的部分。
另外還有可能是在code gen階段,比如場景二第一遍生成的機器偽碼為:
move ax, ivaladd ax, sizeof(some_type)sub ax, 0xfffffffjgz ax. xxxx那麼第二遍說不定就有個pinhole優化看到一個add接著一個sub,且參數都是立即數,那麼就立刻優化掉了。需要指出的是,這種常量摺疊和與之伴隨的運算順序調整對浮點計算來說比較tricky,一般編譯器都有些選項讓你選擇是「比較激進的優化」還是「比較保留計算精度」。或許這個就撤遠了。分別-S -O[0-4]編譯,對比下結果。
int c1(uint32 a)
{ return a+1 &> 0xffffffff;}int c2(uint32 a)
{ return a &> 0xffffffff-1;}main(){return c1(0-1) == c2(0-1);}前面的回答基本差不多了,還是要看編譯器和優化等級來的。順便,竊以為針對邊界情況和未定義行為作優化多少有點耍流氓的意思。
推薦閱讀:
※C++如何判斷一個整數溢出?
※c語言中一個函數的聲明和定義有區別嗎?
※C++中生成隨機數的問題?
※*a.b()是什麼意思,運算符順序是怎麼看的?
※CS、360、這些軟體的根目錄下有很多不同類型的文件。在沒有VS、JDK這些「程序設計語言」的「支持?