问题

这个代码int函数无返回值且开-O2,for循环会无限循环,是什么原因?

回答
好的,我们来深入分析一下这个问题。你遇到的情况是:一个 `int` 函数,启用了 `O2` 优化,然后在函数内部存在一个 `for` 循环,导致无限循环,而且这个 `int` 函数声明为无返回值,但这本身并不是导致无限循环的直接原因。

核心问题分析:O2 优化与无限循环

`O2` 是 GCC/Clang 等编译器中一个相当积极的优化级别。它的目标是在保证程序正确性的前提下,生成最快的机器码。然而,当代码中存在一些不符合编译器预期或编译器无法完全理解的模式时,积极的优化反而可能引入问题,特别是与控制流和副作用相关的部分。

为什么一个无返回值的 `int` 函数里会有无限循环?

首先,澄清一点:即使函数声明为 `int` 类型,它也可以不显式地 `return` 一个值。在这种情况下,根据 C/C++ 标准,当函数执行结束时,如果存在返回值声明但没有 `return` 语句,程序的行为是未定义的。但是,这与无限循环本身没有直接的因果关系。无限循环是函数体内逻辑执行的结果。

那么,无限循环是如何发生的呢?通常有以下几种可能,尤其是在 `O2` 优化下:

1. 循环条件判断的副作用被优化掉了,或者编译器对副作用的理解与实际不符。
这是最常见的原因。编译器在 `O2` 下会进行大量的代码重排、死代码删除、循环不变外提等操作。如果你的 `for` 循环依赖于某个变量在每次迭代中被修改来改变循环条件,而编译器认为这个修改没有产生“可见的”副作用(例如,没有写入全局变量、没有进行 I/O 操作),它可能会:
将循环条件判断提前或延后: 如果编译器判断某个变量在循环中被修改,但这个修改只影响了局部变量的计算,而最终的循环条件判断似乎与这个修改无关,编译器可能会认为这个修改是冗余的。
将整个循环内容提升到循环外面: 如果编译器认为循环体的计算结果对于最终的返回值(即使函数无返回值,它仍然可能参与到其他计算中,或者编译器在优化过程中会假设它有某种“值”)是固定的,或者循环体内的计算结果会被后续的计算所覆盖,它可能会尝试将循环内容“展开”或者完全移除。
移除变量的更新: 如果编译器发现一个变量在循环中被更新,但这个更新似乎没有影响到循环的退出条件,或者编译器认为这个变量的最终值并不重要,它可能会移除对这个变量的更新操作,导致循环条件永远不满足退出条件。

举个例子来说明:

```c++
int my_function() {
int count = 0;
for (int i = 0; i < 10; i++) {
// 假设这里有一些复杂的计算,
// 但是其中一个操作是 i++
// 编译器在 O2 下可能会观察到:
// 1. i 的最终值似乎不影响函数的任何外部行为(例如没有打印,没有返回,没有改变全局变量)
// 2. 循环的退出条件 i < 10 似乎是恒定的(如果 i 的增量被优化了)
// 3. 如果 i 的增量只用于自身的局部计算,并且这个计算的结果又被覆盖了,编译器可能就移除掉 i++
count++; // 假设这里有一个 count++ 操作
}
// ... 其他代码 ...
// no return statement, which is allowed for void functions, but here it's int
// which is technically undefined behavior for int returning functions without return.
// But the core issue is the loop itself.
return 0; // Let's assume there's a return to make it a valid int function
}
```

在上面的伪代码里,如果 `i++` 这个操作本身没有对任何外部可见的副作用(例如,没有影响到 `count` 的最终值,或者 `count` 的最终值本身也没有被进一步使用),编译器可能会认为 `i++` 是冗余的,从而将其移除。一旦 `i` 不再增加,`i < 10` 这个条件就可能永远为真,导致无限循环。

2. 循环依赖于一个只有在特定(未发生)条件下才会改变的变量。
如果你的循环条件依赖于一个变量,而这个变量的修改逻辑很复杂,并且在某些情况下,这个修改可能永远不会发生,那么在没有 `O2` 优化时,你可能还能看到这个变量被正确修改。但 `O2` 优化器可能会通过静态分析发现,在某些执行路径下,这个变量的修改逻辑被绕过了,从而导致循环条件永远不满足退出。

3. 浮点数运算的精度问题导致循环条件永远不满足。
尽管你说的是 `int` 函数,但如果循环条件涉及到浮点数(即使是间接的,例如通过 `int` 转换),浮点数的精度问题也可能导致一个本应终止的循环永远持续下去。

4. 编译器对某些 C/C++ 标准行为的“更严格”或“更宽松”的解释。
有时,标准是允许一定程度的模糊性的。编译器在优化时,可能会选择一种它认为更高效的解释。例如,对于“未定义行为”(Undefined Behavior, UB),编译器拥有最大的自由度,它可以假设 UB 不会发生,或者将 UB 的结果推断为对优化最有利的(比如,假设某个条件永远为真或永远为假)。

为什么 `int` 函数无返回值会加剧这个问题?

如前所述,声明为 `int` 函数但没有 `return` 语句,在 C/C++ 中是未定义行为。一个有经验的程序员会知道这一点,并且会避免这种情况(除非是想让编译器发出警告)。

当存在未定义行为时,编译器优化器就拥有了“免死金牌”。它可以自由地重排、删除、甚至臆测代码的行为。如果你的无限循环是由于你代码中的某个地方存在 UB,并且这个 UB 在 `O2` 优化下被编译器“利用”了(例如,编译器推断出某个分支永远不会执行,从而将依赖于该分支的代码移除),那么这个 UB 就会导致你看到的异常行为。

如何定位和解决问题?

1. 逐步降低优化级别: 这是最直接的方法。
先尝试 `O1`。如果问题消失,说明 `O2` 的某些特定优化导致了问题。
再尝试 `O0` (无优化)。如果 `O0` 下正常,那基本可以确定是编译器优化导致。
尝试 `Os` (优化大小)。有时候优化大小和优化速度会导致不同的行为。

2. 添加最小化的可复现代码: 将你的函数以及相关的全局变量、外部依赖都提取出来,创建一个独立的、最小化的例子。这样可以帮助你专注于问题本身,排除其他干扰。

3. 使用调试器(配合 `O0` 或 `O1`): 在没有优化的情况下,用调试器逐步执行你的代码。观察循环变量的变化,检查循环条件是否在预期中改变。
设置断点: 在循环的开始、循环体的关键位置、以及循环条件的判断处设置断点。
观察变量: 查看循环控制变量的值在每次迭代中是否按预期递增或递减。
单步执行: 精确地追踪代码的执行流程。

4. 检查循环条件和控制变量:
确保循环的控制变量的更新逻辑是清晰的,并且在每次迭代中都会被执行。
避免使用浮点数作为循环的终止条件,除非你知道自己在做什么。
如果循环依赖于某个标志位或状态,确保该状态的更新路径是完备的。

5. 寻找未定义行为(UB):
检查数组越界访问。
检查空指针解引用。
检查使用未初始化的变量。
检查整数溢出(虽然 C++20 对有符号整数溢出有了定义,但在 C++11/14/17 中仍是 UB)。
对于 `int` 函数无返回值的情况,即使它不直接导致无限循环,也应该修正它,给它一个 `return` 语句。例如,`return 0;` 或者根据函数逻辑返回一个有意义的值。

6. 编译器警告是你的朋友: 始终以 `Wall Wextra pedantic` 的最高级别编译你的代码,并认真处理所有警告。有些警告虽然不是错误,但可能预示着潜在的问题,尤其是在涉及优化时。

总结来说,这个问题的根源在于编译器 `O2` 优化对循环控制逻辑的某种“激进”处理,很可能是因为编译器认为循环中的某个变量更新没有“可见的”副作用,从而将其移除或重排,导致循环条件永远不满足退出。同时,`int` 函数无返回值虽然不是直接原因,但它构成了未定义行为,为编译器的优化提供了更大的自由度,可能间接导致了问题的发生。

要解决它,你需要像侦探一样,一步步地排除可能性,从降低优化级别开始,到仔细检查代码中的副作用和潜在的未定义行为。

网友意见

user avatar

很显然这就是gcc的bug。而且高版本也修复了。恭喜题主发现了bug。

那些拿ub说事的我也只能说先去看标准再发言。

至于C语言为什么会被设计得允许不写return。因为最初设计的时候C语言本就允许使用其他方式填返回值。比如早年间只要往AX寄存器写一个值然后函数退出,那个值就是返回值了,而且这个值是可以直接用嵌入式汇编去写的,函数没有return语句。

对最初的C语言来说,甚至你是否声明函数返回值都没关系,不声明的话默认为int,调用方用了返回值就会去检查返回值,调用方没用返回值就不检查。

而很多特性需要保持兼容性,也就是说「只要调用方不使用返回值你就可以不写return」这种C特性是需要保留的。也就意味着这不是ub,而是gcc优化器的bug。

user avatar

建议大家在说一段代码是UB之前,不说查看一下标准原文,最少也Google一下。


我看了一下C11标准,这段代码应该不是UB,所以我倾向于这是GCC的一个bug。并且我用最新的GCC11试了上面的例子,也无法复现,说明这个bug很大可能已经被修复了。


下面是标准原文[1]Section 6.9.1, P174-12:

If the } that terminates a function is reached, and the value of the function call is used by the caller, the behavior is undefined.

注意我标粗的那一句,用的是and。意味着只有在返回值被使用的时候才是UB,显然题主不是这个情况。

更新:

谢谢大家指正:这段代码如果看成是c++的话,确实是UB,与C有别。

参考

  1. ^ISO/IEC 9899:201x http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1548.pdf
user avatar

从一个返回值类型不为 void 的函数返回,但是却没有指定返回值,是 undefined behavior。

C/C++ 编译器可以利用 undefined behavior 执行非常激进的优化。例如,有一个优化策略是,编译器可以假定程序中任意一条路径中没有任何的 undefined behavior。在本例中,参考这个策略,编译器会认为 test 函数中的 for 循环永不退出(因为只要退出,就会产生 UB)。因此,编译器在高优化级别下可能将 test 中的循环直接优化为一个死循环。


更新:我来尝试详细展开说明一下编译器执行本例中的优化的过程。我的编译环境是 Ubuntu 20.04,编译器是 clang++ 10.0.0。

首先看一下优化之前 clang 生成的 IR:

       ; Function Attrs: noinline optnone uwtable define dso_local i32 @_Z4testv() #0 {   %1 = alloca i32, align 4   store i32 0, i32* %1, align 4   br label %2  2:                                                ; preds = %8, %0   %3 = load i32, i32* %1, align 4   %4 = icmp slt i32 %3, 1010   br i1 %4, label %5, label %11  5:                                                ; preds = %2   %6 = load i32, i32* %1, align 4   %7 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([7 x i8], [7 x i8]* @.str, i64 0, i64 0), i32 %6, i32 1010)   br label %8  8:                                                ; preds = %5   %9 = load i32, i32* %1, align 4   %10 = add nsw i32 %9, 1   store i32 %10, i32* %1, align 4   br label %2  11:                                               ; preds = %2   call void @llvm.trap()   unreachable }     

基本块 %2 是检查 loop condition,基本块 %5 是 loop body,基本块 %8 是更新 loop variable。有趣的是基本块 %11,这是循环结束后第一个执行的基本块,其中除去一条 intrinsic call 外只有一条 unreachable 指令。这条指令告诉 LLVM 优化器,这里的代码在实际运行时不可达,优化器可以借此搞点事情。clang 生成这样的代码正是基于之前介绍的假设,即程序的所有路径均不包含 UB;换言之,如果代码里面包含了 UB,那么这一坨问题代码一定不可达。

接下来优化器开始搞事。这里我们用的是 clang 提供的 -O1 优化级别中的 optimization pass,由于 pass 较多,我只挑重点的介绍。只需要两步就可以优化出死循环(其实只需要一步就可以,但我顺着 clang 的优化顺序来)。

首先,--sroa尝试将 non-escape 的 allocation 提升为 SSA value。做完这一步的代码会瞬间清爽许多,因为 --sroa 几乎将所有的 allocation 全部消除了:

       ; Function Attrs: uwtable define dso_local i32 @_Z4testv() #0 {   br label %1  1:                                                ; preds = %5, %0   %2 = phi i32 [ 0, %0 ], [ %7, %5 ]   %3 = icmp slt i32 %2, 1010   br i1 %3, label %5, label %4  4:                                                ; preds = %1   unreachable  5:                                                ; preds = %1   %6 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([7 x i8], [7 x i8]* @.str, i64 0, i64 0), i32 %2, i32 1010)   %7 = add nsw i32 %2, 1   br label %1 }      

然后,--simplifycfg 将尝试做基本块合并。这是最核心的一个优化过程。首先,--simplifycfg 看到基本块 %4 是 unreachable,而在基本块 %1 中若 %3 为 false 则会跳入基本块 %4,因此 --simplifycfg 消除基本块 %4 并向 %1 中加入一个 intrinsic call:

        ; Function Attrs: uwtable define dso_local i32 @_Z4testv() #0 {   br label %1  1:                                                ; preds = %5, %0   %2 = phi i32 [ 0, %0 ], [ %7, %5 ]   %3 = icmp slt i32 %2, 1010   call void @llvm.assume(i1 %3)   br label %5  5:                                                ; preds = %1   %6 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([7 x i8], [7 x i8]* @.str, i64 0, i64 0), i32 %2, i32 1010)   %7 = add nsw i32 %2, 1   br label %1 }     

新加入的 intrinsic call(@llvm.assume(i1 %3))即提示优化器 %3 的值应该为 true,优化器可能还可以利用这一点信息进行更进一步的优化。但在本例中这个 intrinsic call 没有发挥作用。

然后,--simplifycfg 进一步执行基本块合并,将基本块 %5 内联进基本块 %1

       ; Function Attrs: uwtable define dso_local i32 @_Z4testv() local_unnamed_addr #0 {   br label %1  1:                                                ; preds = %1, %0   %2 = phi i32 [ 0, %0 ], [ %5, %1 ]   %3 = icmp ult i32 %2, 1010   call void @llvm.assume(i1 %3)   %4 = call i32 (i8*, ...) @printf(i8* nonnull dereferenceable(1) getelementptr inbounds ([7 x i8], [7 x i8]* @.str, i64 0, i64 0), i32 %2, i32 1010)   %5 = add nuw nsw i32 %2, 1   br label %1 }     

至此优化完成。可以看到,基本块 %1 自身构成一个死循环。


再次更新:

编译器实际上可以利用 undefined behavior 做非常多的优化。CppCon 2016 上面有一个很好的 talk 详细介绍了这个点:

类似的话题

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有