为何主流C编译器会在noreturn函数中保存寄存器?能否取消该操作?
这确实是个很细致的观察——你提到主要用Clang(版本20.1.7,目标平台x86_64-pc-windows-msvc),也测试了MinGW GCC和MSVC,发现它们给__attribute__((noreturn))标记的函数生成了寄存器保存的代码,但因为函数永远不会返回,这些寄存器根本没机会被恢复,看起来完全是无用的操作对吧?我们结合你给出的测试环境、代码和汇编输出,一步步拆解这个问题:
你的测试环境与代码
首先是你使用的Clang环境信息:
E:\code\test>clang -v clang version 20.1.7 Target: x86_64-pc-windows-msvc Thread model: posix InstalledDir: D:\installable\LLVM\bin
用来测试的C代码:
int get_num(); int interface(int x); __attribute__((noreturn)) void exit(); __attribute__((noreturn)) void entry(int a, int b, int c, int d) { interface(get_num() * a * b * c * d); exit(); }
你用以下命令生成汇编代码:
E:\code\test>clang -S -O3 -ffreestanding test.c
生成的汇编中,entry函数开头有一串看起来无用的寄存器push操作:
entry: # @entry # %bb.0: pushq %rsi pushq %rdi pushq %rbp pushq %rbx subq $40, %rsp # ... 后续调用get_num、乘法计算、调用interface和exit
为什么编译器要生成这些push指令?
核心原因主要有两点:
调用约定的约束与编译器实现的通用性
你测试的是Windows x64平台,在这个平台的调用约定中,rbx、rbp、rdi、rsi属于被调用者保存寄存器——也就是说,如果一个函数要使用这些寄存器,必须先保存它们的原值,再在返回前恢复。
虽然entry是noreturn函数不会返回,但编译器的函数框架生成逻辑是通用的:它只会检查函数是否使用了被调用者保存的寄存器,只要用了就生成保存代码;而因为函数标记了noreturn,恢复寄存器的pop代码会被优化掉,但保存的push代码会保留。
编译器不会为noreturn函数单独重写一套特殊的逻辑,因为这会增加实现复杂度,而几个push指令的开销在绝大多数场景下都可以忽略,收益远小于付出的成本。内部函数调用的上下文正确性
你的entry函数内部调用了get_num和interface这些普通函数,这些调用必须严格遵守调用约定。保存寄存器的操作是为了确保这些内部调用的上下文正确,避免破坏调用者的寄存器状态——即使最终不会恢复,保存操作本身是符合调用约定规范的。
能不能取消这些无用的push操作?
是可以的,但属于比较边缘的场景,需要特殊处理:
使用
__attribute__((naked))属性
这个属性会告诉编译器不要自动生成任何函数开头的prologue和结尾的epilogue代码,完全由你手动控制寄存器的使用和参数处理。修改后的函数声明如下:__attribute__((noreturn, naked)) void entry(int a, int b, int c, int d) { // 这里需要用asm volatile编写符合调用约定的汇编代码 // 编译器不会帮你生成任何寄存器保存或参数传递的逻辑 }这种方式能彻底消除无用的push指令,但代价是你需要手动编写汇编来处理参数、内部调用等逻辑。
手动优化极端场景
如果你坚持用C代码编写,目前没有专门的编译器选项可以直接禁用这些push操作——因为这属于非常小众的优化需求,编译器的默认逻辑是优先保证调用约定的正确性和实现的通用性,而非为边缘场景做极端优化。
总的来说,这些push指令的开销极小,对于绝大多数noreturn函数的使用场景来说完全可以忽略;只有在对代码大小或执行效率有极致要求的无宿主环境(比如内核、嵌入式系统)中,才需要考虑手动消除这些操作。
内容来源于stack exchange

