![](/img/trans.png)
[英]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.