百科问答小站 logo
百科问答小站 font logo



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

  

user avatar   pansz 网友的相关建议: 
      

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

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

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

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

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


user avatar   jks-liu 网友的相关建议: 
      

建议大家在说一段代码是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   lancern 网友的相关建议: 
      

从一个返回值类型不为 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 详细介绍了这个点:




  

相关话题

  做一个不同编程语言之间的converter有没有意义? 
  CodeBlocks 的强大之处在哪里? 
  为什么程序员要使用三元运算符而不是显式写出 if 语句? 
  你目前写出的最大的 Bug 是怎样的? 
  如何由 C# 的「拖控件」阶段继续深入学习? 
  如何评价Linus Torvalds编写的LINUX内核的技术含量? 
  我大一用 30 多行代码写出了 36 以内的进制转换,属于什么水平? 
  如果我发布了一款编程语言,后缀名是.c,那么这能算是一门新的编程语言吗? 
  插件机制实现的原理是什么? 
  如果编程语言有性别?Java、C++、C、C#是男是女?是GAY还是LES? 

前一个讨论
C++可否将父类的对象cast子类,并调用子类的private函数?
下一个讨论
老板要求我不要在公司内打游戏,但是我打游戏是在午休和下班后没有加班补偿机制的加班时间玩的,合理吗?





© 2024-05-20 - tinynew.org. All Rights Reserved.
© 2024-05-20 - tinynew.org. 保留所有权利