TL:DR:gcc错过了优化。
noreturn
向编译器保证该函数不会返回。这可以进行优化,并且在编译器难以证明循环永远不会退出或难以证明没有返回函数路径的情况下特别有用。
main
即使func()
返回,GCC仍已进行优化以使函数结束(即使使用的默认值-O0
(最低优化级别)也是如此)。
func()
本身的输出可以认为是错过的优化;它可能会在函数调用之后忽略所有内容(因为不返回调用是函数本身唯一的方法noreturn
)。这不是一个很好的例子,因为printf
它是已知可以正常返回的标准C函数(除非您setvbuf
给出stdout
出现段错误的缓冲区?)
让我们使用编译器不知道的其他函数。
void ext(void);
int foo;
_Noreturn void func(int *p, int a) {
ext();
*p = a;
foo = 1;
}
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()
用with尾部调用jmp ext
。但是gcc不会尾noreturn
调用函数,因为那样会丢失回溯信息,例如abort()
。显然,内联它们是可以的。
Gcc可以通过在此mov
之后省略商店来进行优化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
不保存/恢复它们。
Gcc for ARM32确实可以做到这一点,但是仍然发出指令以其他方式返回。因此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
,那正是我们得到的。
任何以返回值结尾的基本块都可以认为永远不会到达。