TL:DR: 这是 gcc 错过的优化。
noreturn
是对编译器的一个承诺,即函数不会返回。这允许优化,并且在编译器难以证明循环永远不会退出或以其他方式证明没有通过返回函数的路径的情况下尤其有用。
如果返回, GCC 已经优化main
到函数的末尾,即使使用看起来像您使用func()
的默认(最低优化级别)。-O0
输出func()
本身可以被认为是错过的优化;它可以在函数调用之后省略所有内容(因为调用不返回是函数本身的唯一方法noreturn
)。这不是一个很好的例子,因为printf
它是一个已知可以正常返回的标准 C 函数(除非你setvbuf
提供stdout
一个会出现段错误的缓冲区?)
让我们使用编译器不知道的不同函数。
void ext(void);
//static
int foo;
_Noreturn void func(int *p, int a) {
ext();
*p = a; // using function args after a function call
foo = 1; // requires save/restore of registers
}
void bar() {
func(&foo, 3);
}
(Godbolt 编译器资源管理器上的代码 + x86-64 asm 。)
gcc7.2 的输出bar()
很有趣。它内联func()
,并消除了foo=3
死存储,只留下:
bar:
sub rsp, 8 ## align the stack
call ext
mov DWORD PTR foo[rip], 1
## fall off the end
Gcc 仍然假设它ext()
会返回,否则它可能只是ext()
用jmp ext
. 但是 gcc 不会尾noreturn
调用函数,因为这会丢失诸如abort()
. 不过,显然内联它们是可以的。
Gcc 也可以通过在之后省略mov
store来优化call
。如果ext
返回,则程序被冲洗,因此生成任何代码都没有意义。Clang 确实在bar()
/中进行了优化main()
。
func
本身更有趣,而且错过了更大的优化。
gcc 和 clang 都发出几乎相同的东西:
func:
push rbp # save some call-preserved regs
push rbx
mov ebp, esi # save function args for after ext()
mov rbx, rdi
sub rsp, 8 # align the stack before a call
call ext
mov DWORD PTR [rbx], ebp # *p = a;
mov DWORD PTR foo[rip], 1 # foo = 1
add rsp, 8
pop rbx # restore call-preserved regs
pop rbp
ret
这个函数可以假设它不会返回,并且使用rbx
并且rbp
不保存/恢复它们。
ARM32 的 Gcc 实际上是这样做的,但仍会发出指令以干净地返回。因此noreturn
,在 ARM32 上实际返回的函数将破坏 ABI 并在调用者或更高版本中导致难以调试的问题。(未定义的行为允许这样做,但这至少是一个实施质量问题:https ://gcc.gnu.org/bugzilla/show_bug.cgi?id=82158 。)
在 gcc 无法证明函数是否返回的情况下,这是一个有用的优化。(不过,当函数确实返回时,这显然是有害的。当 Gcc 确定 noreturn 函数确实返回时会发出警告。)其他 gcc 目标体系结构不这样做。这也是一个错过的优化。
但是 gcc 还远远不够:优化返回指令(或用非法指令替换它)将节省代码大小并保证嘈杂的失败而不是无声的损坏。
如果你要优化掉ret
, 优化掉只有在函数返回时才需要的所有东西才有意义。
因此,func()
可以编译为:
sub rsp, 8
call ext
# *p = a; and so on assumed to never happen
ud2 # optional: illegal insn instead of fall-through
存在的所有其他指令都是错过的优化。如果ext
被声明noreturn
,这正是我们得到的。
任何以 return 结尾的基本块都可以被假定为永远不会到达。