[英]compiler warning “warning: iteration 10u invokes undefined behavior [-Waggressive-loop-optimizations]” for M[i] ^ k;
[英]Why does this loop produce “warning: iteration 3u invokes undefined behavior” and output more than 4 lines?
編譯這個:
#include <iostream>
int main()
{
for (int i = 0; i < 4; ++i)
std::cout << i*1000000000 << std::endl;
}
和gcc
產生以下警告:
warning: iteration 3u invokes undefined behavior [-Waggressive-loop-optimizations]
std::cout << i*1000000000 << std::endl;
^
我知道有一個有符號整數溢出。
我無法得到的是為什么i
值被溢出操作破壞了?
我已經閱讀了為什么帶有 GCC 的 x86 上的整數溢出會導致無限循環的答案? ,但我仍然不清楚為什么會發生這種情況 - 我認為“未定義”意味着“任何事情都可能發生”,但是這種特定行為的根本原因是什么?
在線: http : //ideone.com/dMrRKR
編譯器: gcc (4.8)
有符號整數溢出(嚴格來說,沒有“無符號整數溢出”這樣的東西)意味着未定義的行為。 這意味着任何事情都可能發生,討論為什么會在 C++ 規則下發生是沒有意義的。
C++11 草案 N3337:§5.4: 1
如果在對表達式求值期間,結果未在數學上定義或不在其類型的可表示值范圍內,則行為未定義。 [注意:大多數現有的 C++ 實現都忽略整數溢出。 除以零的處理、使用零除數形成余數以及所有浮點異常的處理因機器而異,通常可以通過庫函數進行調整。 ——尾注]
您使用g++ -O3
編譯的代碼會發出警告(即使沒有-Wall
)
a.cpp: In function 'int main()':
a.cpp:11:18: warning: iteration 3u invokes undefined behavior [-Waggressive-loop-optimizations]
std::cout << i*1000000000 << std::endl;
^
a.cpp:9:2: note: containing loop
for (int i = 0; i < 4; ++i)
^
我們可以分析程序正在做什么的唯一方法是讀取生成的匯編代碼。
這是完整的程序集列表:
.file "a.cpp"
.section .text$_ZNKSt5ctypeIcE8do_widenEc,"x"
.linkonce discard
.align 2
LCOLDB0:
LHOTB0:
.align 2
.p2align 4,,15
.globl __ZNKSt5ctypeIcE8do_widenEc
.def __ZNKSt5ctypeIcE8do_widenEc; .scl 2; .type 32; .endef
__ZNKSt5ctypeIcE8do_widenEc:
LFB860:
.cfi_startproc
movzbl 4(%esp), %eax
ret $4
.cfi_endproc
LFE860:
LCOLDE0:
LHOTE0:
.section .text.unlikely,"x"
LCOLDB1:
.text
LHOTB1:
.p2align 4,,15
.def ___tcf_0; .scl 3; .type 32; .endef
___tcf_0:
LFB1091:
.cfi_startproc
movl $__ZStL8__ioinit, %ecx
jmp __ZNSt8ios_base4InitD1Ev
.cfi_endproc
LFE1091:
.section .text.unlikely,"x"
LCOLDE1:
.text
LHOTE1:
.def ___main; .scl 2; .type 32; .endef
.section .text.unlikely,"x"
LCOLDB2:
.section .text.startup,"x"
LHOTB2:
.p2align 4,,15
.globl _main
.def _main; .scl 2; .type 32; .endef
_main:
LFB1084:
.cfi_startproc
leal 4(%esp), %ecx
.cfi_def_cfa 1, 0
andl $-16, %esp
pushl -4(%ecx)
pushl %ebp
.cfi_escape 0x10,0x5,0x2,0x75,0
movl %esp, %ebp
pushl %edi
pushl %esi
pushl %ebx
pushl %ecx
.cfi_escape 0xf,0x3,0x75,0x70,0x6
.cfi_escape 0x10,0x7,0x2,0x75,0x7c
.cfi_escape 0x10,0x6,0x2,0x75,0x78
.cfi_escape 0x10,0x3,0x2,0x75,0x74
xorl %edi, %edi
subl $24, %esp
call ___main
L4:
movl %edi, (%esp)
movl $__ZSt4cout, %ecx
call __ZNSolsEi
movl %eax, %esi
movl (%eax), %eax
subl $4, %esp
movl -12(%eax), %eax
movl 124(%esi,%eax), %ebx
testl %ebx, %ebx
je L15
cmpb $0, 28(%ebx)
je L5
movsbl 39(%ebx), %eax
L6:
movl %esi, %ecx
movl %eax, (%esp)
addl $1000000000, %edi
call __ZNSo3putEc
subl $4, %esp
movl %eax, %ecx
call __ZNSo5flushEv
jmp L4
.p2align 4,,10
L5:
movl %ebx, %ecx
call __ZNKSt5ctypeIcE13_M_widen_initEv
movl (%ebx), %eax
movl 24(%eax), %edx
movl $10, %eax
cmpl $__ZNKSt5ctypeIcE8do_widenEc, %edx
je L6
movl $10, (%esp)
movl %ebx, %ecx
call *%edx
movsbl %al, %eax
pushl %edx
jmp L6
L15:
call __ZSt16__throw_bad_castv
.cfi_endproc
LFE1084:
.section .text.unlikely,"x"
LCOLDE2:
.section .text.startup,"x"
LHOTE2:
.section .text.unlikely,"x"
LCOLDB3:
.section .text.startup,"x"
LHOTB3:
.p2align 4,,15
.def __GLOBAL__sub_I_main; .scl 3; .type 32; .endef
__GLOBAL__sub_I_main:
LFB1092:
.cfi_startproc
subl $28, %esp
.cfi_def_cfa_offset 32
movl $__ZStL8__ioinit, %ecx
call __ZNSt8ios_base4InitC1Ev
movl $___tcf_0, (%esp)
call _atexit
addl $28, %esp
.cfi_def_cfa_offset 4
ret
.cfi_endproc
LFE1092:
.section .text.unlikely,"x"
LCOLDE3:
.section .text.startup,"x"
LHOTE3:
.section .ctors,"w"
.align 4
.long __GLOBAL__sub_I_main
.lcomm __ZStL8__ioinit,1,1
.ident "GCC: (i686-posix-dwarf-rev1, Built by MinGW-W64 project) 4.9.0"
.def __ZNSt8ios_base4InitD1Ev; .scl 2; .type 32; .endef
.def __ZNSolsEi; .scl 2; .type 32; .endef
.def __ZNSo3putEc; .scl 2; .type 32; .endef
.def __ZNSo5flushEv; .scl 2; .type 32; .endef
.def __ZNKSt5ctypeIcE13_M_widen_initEv; .scl 2; .type 32; .endef
.def __ZSt16__throw_bad_castv; .scl 2; .type 32; .endef
.def __ZNSt8ios_base4InitC1Ev; .scl 2; .type 32; .endef
.def _atexit; .scl 2; .type 32; .endef
我什至幾乎看不懂匯編,但即使我能看到addl $1000000000, %edi
行。 結果代碼看起來更像
for(int i = 0; /* nothing, that is - infinite loop */; i += 1000000000)
std::cout << i << std::endl;
@TC 的評論:
我懷疑它是這樣的:(1)因為
i
的任何大於 2 的值的每次迭代都有未定義的行為 ->(2)我們可以假設i <= 2
用於優化目的 ->(3)循環條件總是true -> (4) 它被優化為無限循環。
給了我將 OP 代碼的匯編代碼與以下代碼的匯編代碼進行比較的想法,沒有未定義的行為。
#include <iostream>
int main()
{
// changed the termination condition
for (int i = 0; i < 3; ++i)
std::cout << i*1000000000 << std::endl;
}
而且,事實上,正確的代碼具有終止條件。
; ...snip...
L6:
mov ecx, edi
mov DWORD PTR [esp], eax
add esi, 1000000000
call __ZNSo3putEc
sub esp, 4
mov ecx, eax
call __ZNSo5flushEv
cmp esi, -1294967296 // here it is
jne L7
lea esp, [ebp-16]
xor eax, eax
pop ecx
; ...snip...
不幸的是,這是編寫錯誤代碼的后果。
幸運的是,您可以利用更好的診斷和更好的調試工具——這就是它們的用途:
啟用所有警告
-Wall
是 gcc 選項,它啟用所有有用的警告而沒有誤報。 這是您應該始終使用的最低限度。
gcc 有許多其他警告選項,但是,它們沒有通過-Wall
啟用,因為它們可能會在誤報時發出警告
不幸的是,Visual C++ 在提供有用警告的能力方面落后了。 至少 IDE 默認啟用了一些。
使用調試標志進行調試
-ftrapv
在溢出時捕獲程序,-fcatch-undefined-behavior
捕獲了很多未定義行為的實例(注意: "a lot of" != "all of them"
)我有一個不是我寫的程序,需要明天發貨! 幫助!!!!!!111oneone
使用 gcc 的-fwrapv
此選項指示編譯器假設加法、減法和乘法的有符號算術溢出使用二進制補碼表示環繞。
1 - 此規則不適用於“無符號整數溢出”,如第 3.9.1.4 節所述
聲明為無符號的無符號整數應遵守算術模 2 n的法則,其中 n 是該特定整數大小的值表示中的位數。
並且例如UINT_MAX + 1
結果是數學定義的 - 由算術模 2 n的規則
簡短的回答, gcc
專門記錄了這個問題,我們可以在gcc 4.8 發行說明中看到它說(強調我的未來):
GCC 現在使用更積極的分析來使用語言標准強加的約束來推導出循環迭代次數的上限。 這可能會導致不符合要求的程序不再按預期工作,例如 SPEC CPU 2006 464.h264ref 和 416.gamess。 添加了一個新選項 -fno-aggressive-loop-optimizations 以禁用此積極分析。 在某些已知迭代次數恆定的循環中,但已知在到達之前或最后一次迭代期間在循環中發生了未定義的行為,GCC 將警告循環中的未定義行為,而不是推導出迭代次數的下上限為循環。 可以使用 -Wno-aggressive-loop-optimizations 禁用警告。
事實上,如果我們使用-fno-aggressive-loop-optimizations
無限循環行為應該停止,並且在我測試過的所有情況下都是如此。
通過查看 C++ 標准草案第5
節表達式第4段,知道有符號整數溢出是未定義的行為,答案很長,它說:
如果在對表達式求值期間,結果未在數學上定義或不在其類型的可表示值范圍內,則行為為 undefined 。 [ 注意:大多數現有的 C++ 實現都忽略整數溢出。 除以零的處理,使用零除數形成余數,以及所有浮點異常在機器之間有所不同,通常可以通過庫函數進行調整。 ——尾注
我們知道標准說未定義的行為是不可預測的,根據定義附帶的注釋說:
[注意:當本國際標准省略任何明確的行為定義或程序使用錯誤的構造或錯誤的數據時,可能會出現未定義的行為。 允許的未定義行為的范圍從完全忽略情況並產生不可預測的結果,在翻譯或程序執行期間以環境特征的文件化方式(有或沒有發布診斷消息),到終止翻譯或執行(通過發布診斷消息)。 許多錯誤的程序結構不會產生未定義的行為; 他們需要被診斷。 ——尾注]
但是gcc
優化器到底能做什么來把它變成一個無限循環呢? 這聽起來很古怪。 但幸運的是gcc
在警告中給了我們一個線索來解決它:
warning: iteration 3u invokes undefined behavior [-Waggressive-loop-optimizations]
std::cout << i*1000000000 << std::endl;
^
線索是Waggressive-loop-optimizations
,這是什么意思? 對我們來說幸運的是,這不是這種優化第一次以這種方式破壞代碼,我們很幸運,因為John Regehr在GCC pre-4.8 Breaks Broken SPEC 2006 Benchmarks文章中記錄了一個案例,其中顯示了以下代碼:
int d[16];
int SATD (void)
{
int satd = 0, dd, k;
for (dd=d[k=0]; k<16; dd=d[++k]) {
satd += (dd < 0 ? -dd : dd);
}
return satd;
}
文章說:
未定義的行為是在退出循環之前訪問 d[16]。 在 C99 中,創建一個指向數組末尾后一個位置的元素的指針是合法的,但該指針不能被取消引用。
后來說:
詳細來說,這是發生了什么。 AC 編譯器在看到 d[++k] 時,可以假設 k 的遞增值在數組邊界內,否則會發生未定義的行為。 對於這里的代碼, GCC 可以推斷出 k 的范圍是 0..15。 稍后,當 GCC 看到 k<16 時,它對自己說:“啊哈——這個表達式總是正確的,所以我們有一個無限循環。” 這里的情況,編譯器使用定義良好的假設來推斷有用的數據流事實,
所以編譯器在某些情況下必須做的是假設因為有符號整數溢出是未定義的行為,那么i
必須始終小於4
,因此我們有一個無限循環。
他解釋說,這與臭名昭著的Linux 內核空指針檢查刪除非常相似,其中看到以下代碼:
struct foo *s = ...;
int x = s->f;
if (!s) return ERROR;
gcc
推斷,因為s
在s->f;
被尊重s->f;
並且由於取消引用空指針是未定義的行為,因此s
不能為空,因此優化了下一行的if (!s)
檢查。
這里的教訓是,現代優化器非常熱衷於利用未定義的行為,而且很可能只會變得更加激進。 很明顯,通過幾個例子,我們可以看到優化器做了一些對程序員來說完全不合理的事情,但從優化器的角度回想起來是有道理的。
tl;dr代碼生成一個測試,整數+正整數==負整數。 通常優化器不會優化這個,但是在接下來使用std::endl
的特定情況下,編譯器會優化這個測試。 我還沒有弄清楚endl
有什么特別之處。
從 -O1 和更高級別的匯編代碼,很明顯 gcc 將循環重構為:
i = 0;
do {
cout << i << endl;
i += NUMBER;
}
while (i != NUMBER * 4)
正常工作的最大值是715827882
,即 floor( INT_MAX/3
)。 在組裝片斷-O1
是:
L4:
movsbl %al, %eax
movl %eax, 4(%esp)
movl $__ZSt4cout, (%esp)
call __ZNSo3putEc
movl %eax, (%esp)
call __ZNSo5flushEv
addl $715827882, %esi
cmpl $-1431655768, %esi
jne L6
// fallthrough to "return" code
請注意, -1431655768
是 2 的補碼中的4 * 715827882
。
點擊-O2
優化為以下內容:
L4:
movsbl %al, %eax
addl $715827882, %esi
movl %eax, 4(%esp)
movl $__ZSt4cout, (%esp)
call __ZNSo3putEc
movl %eax, (%esp)
call __ZNSo5flushEv
cmpl $-1431655768, %esi
jne L6
leal -8(%ebp), %esp
jne L6
// fallthrough to "return" code
所以所做的優化只是將addl
向上移動。
如果我們715827883
重新編譯,那么除了更改的數字和測試值之外, 715827883
版本是相同的。 但是,-O2 然后進行了更改:
L4:
movsbl %al, %eax
addl $715827883, %esi
movl %eax, 4(%esp)
movl $__ZSt4cout, (%esp)
call __ZNSo3putEc
movl %eax, (%esp)
call __ZNSo5flushEv
jmp L2
有人的地方就有cmpl $-1431655764, %esi
在-O1
,該行已經為去除-O2
。 優化器必須決定將715827883
添加到%esi
永遠不能等於-1431655764
。
這很令人費解。 將其添加到INT_MIN+1
確實會生成預期的結果,因此優化器肯定已經決定%esi
永遠不會是INT_MIN+1
並且我不確定為什么會這樣決定。
在工作示例中,得出將715827882
添加到數字不能等於INT_MIN + 715827882 - 2
結論似乎同樣有效! (這只有在實際發生回繞時才有可能),但它並沒有優化該示例中的線路。
我使用的代碼是:
#include <iostream>
#include <cstdio>
int main()
{
for (int i = 0; i < 4; ++i)
{
//volatile int j = i*715827883;
volatile int j = i*715827882;
printf("%d\n", j);
std::endl(std::cout);
}
}
如果std::endl(std::cout)
被刪除,則優化不再發生。 實際上用std::cout.put('\\n'); std::flush(std::cout);
替換它std::cout.put('\\n'); std::flush(std::cout);
std::cout.put('\\n'); std::flush(std::cout);
也會導致優化不會發生,即使std::endl
是內聯的。
std::endl
的內聯似乎影響了循環結構的早期部分(我不太明白它在做什么,但我會在這里發布以防其他人這樣做):
使用原始代碼和-O2
:
L2:
movl %esi, 28(%esp)
movl 28(%esp), %eax
movl $LC0, (%esp)
movl %eax, 4(%esp)
call _printf
movl __ZSt4cout, %eax
movl -12(%eax), %eax
movl __ZSt4cout+124(%eax), %ebx
testl %ebx, %ebx
je L10
cmpb $0, 28(%ebx)
je L3
movzbl 39(%ebx), %eax
L4:
movsbl %al, %eax
addl $715827883, %esi
movl %eax, 4(%esp)
movl $__ZSt4cout, (%esp)
call __ZNSo3putEc
movl %eax, (%esp)
call __ZNSo5flushEv
jmp L2 // no test
使用std::endl
和-O2
mymanual 內聯:
L3:
movl %ebx, 28(%esp)
movl 28(%esp), %eax
addl $715827883, %ebx
movl $LC0, (%esp)
movl %eax, 4(%esp)
call _printf
movl $10, 4(%esp)
movl $__ZSt4cout, (%esp)
call __ZNSo3putEc
movl $__ZSt4cout, (%esp)
call __ZNSo5flushEv
cmpl $-1431655764, %ebx
jne L3
xorl %eax, %eax
這兩者之間的一個區別是%esi
在原始版本中使用,而%ebx
在第二個版本中使用; 一般來說, %esi
和%ebx
之間定義的語義有什么區別嗎? (我不太了解 x86 匯編)。
在 gcc 中報告此錯誤的另一個示例是,當您有一個循環執行恆定次數的迭代時,但您使用 counter 變量作為索引,該數組的項目數少於該數量,例如:
int a[50], x;
for( i=0; i < 1000; i++) x = a[i];
編譯器可以確定此循環將嘗試訪問數組“a”之外的內存。 編譯器用這個相當神秘的消息抱怨這個:
迭代 xxu 調用未定義的行為 [-Werror=aggressive-loop-optimizations]
我無法得到的是為什么我的值被溢出操作破壞了?
似乎整數溢出發生在第 4 次迭代(對於i = 3
)。 signed
整數溢出調用未定義的行為。 在這種情況下,什么都無法預測。 循環可能只迭代4
次,也可能無限循環或其他任何東西!
結果可能因編譯器而異,甚至對於同一編譯器的不同版本。
本國際標准沒有要求的行為
[注意:當本國際標准省略任何明確的行為定義或程序使用錯誤的構造或錯誤的數據時,可能會出現未定義的行為。 允許的未定義行為的范圍從完全忽略情況並產生不可預測的結果,在翻譯或程序執行期間以環境特征的文件化方式(有或沒有發布診斷消息),到終止翻譯或執行(通過發布診斷消息) 。 許多錯誤的程序結構不會產生未定義的行為; 他們需要被診斷。 ——尾注]
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.