为何Xcode为noexcept修饰的C++函数生成更多汇编指令?
测试基础信息
测试运行环境为Intel架构Mac设备,采用C++11标准编译,测试代码如下:
#include <iostream> void show() //noexcept { std::cout << "Hello, World!\n"; } int main(int argc, const char * argv[]) { show(); return 0; }
测试分别在Debug模式、开启编译优化的Release模式下,对比show函数添加与不添加noexcept修饰符时的汇编代码生成差异。
Debug模式汇编对比
无noexcept修饰版本
未添加noexcept限定的show函数生成汇编如下:
0x100003280 <+0>: pushq %rbp 0x100003281 <+1>: movq %rsp, %rbp 0x100003284 <+4>: movq 0xd75(%rip), %rdi ; (void *)0x00007fff96d36760: std::__1::cout 0x10000328b <+11>: leaq 0xcc2(%rip), %rsi ; "Hello, World!\n" 0x100003292 <+18>: callq 0x100003e2c ; symbol stub for: std::__1::basic_ostream<char..... 0x100003297 <+23>: popq %rbp 0x100003298 <+24>: retq
该段汇编逻辑清晰:创建栈帧、配置cout调用参数、执行输出函数调用、销毁栈帧后返回。
添加noexcept修饰版本
取消注释noexcept修饰符后,show函数生成汇编如下:
0x100003240 <+0>: pushq %rbp 0x100003241 <+1>: movq %rsp, %rbp 0x100003244 <+4>: subq $0x10, %rsp 0x100003248 <+8>: movq 0xdb1(%rip), %rdi ; (void *)0x00007fff96d36760: std::__1::cout 0x10000324f <+15>: leaq 0xcee(%rip), %rsi ; "Hello, World!\n" 0x100003256 <+22>: callq 0x100003e0c ; symbol stub for: std::__1::basic_ostream<char.... 0x10000325b <+27>: jmp 0x100003260 ; <+32> at main.cpp:14:1 0x100003260 <+32>: addq $0x10, %rsp 0x100003264 <+36>: popq %rbp
对比两段代码可发现两处明显差异:
- 添加noexcept后新增了栈空间分配指令
subq $0x10, %rsp,以及对应的栈空间回收指令addq $0x10, %rsp - 代码中存在一条直接跳转至紧邻下一行指令的
jmp指令
待澄清疑问
- 为何添加noexcept修饰后反而生成了额外指令?通常认知中noexcept承诺函数不抛出异常,无需执行栈展开逻辑,生成的指令量应当更少。
- 上述直接跳转至下一行的jmp指令的设计目的是什么?
额外栈操作指令的成因
观察到的额外指令完全是Debug模式无优化编译的特性,和noexcept本身的性能优化逻辑无关。
Debug模式下编译器的首要目标不是生成精简高效的代码,而是保留完整调试信息、匹配源码行号结构、支持单步调试与断点插入,默认不会做任何冗余指令消除。
当函数加上noexcept修饰时,Clang(Mac平台默认C++编译器)会为函数生成异常兜底路径:如果noexcept函数内部违反约定抛出异常,程序需要直接调用std::terminate终止执行,该路径需要预留16字节栈空间存放异常处理的临时上下文,因此会出现对应的栈分配、回收指令。
切换到开启优化的Release模式后再对比就会发现,加不加noexcept的正常执行路径代码长度几乎无差异,noexcept版本反而因为不需要生成异常栈展开的相关元数据,整体二进制体积更小、执行效率更高。Debug模式下的冗余指令不会出现在正式发布的优化版本中。
邻行jmp指令的作用
这条跳转到下一行的jmp同样是Debug模式下为调试体验、编译效率生成的占位指令,没有实际运行时的性能价值。
编译器生成noexcept函数代码时,会预先给异常终止路径预留代码位置:正常执行路径走到函数调用结束位置后,本可以直接执行栈回收、返回逻辑,但编译器会在此处插入一条jmp指令,跳转到函数正常退出的代码块起点,这么做的目的有三点:
- 匹配源码行号映射:jmp指令的注释标注了对应源码位置为
main.cpp:14:1,也就是show函数结束的右大括号位置,单步调试时,执行完cout调用后调试器会停在这条jmp上,对应源码中函数执行完毕准备返回的位置,符合开发者单步调试的行号预期 - 降低增量编译改动量:如果后续给函数内部新增可能抛出异常的逻辑,编译器不需要调整整个函数的指令布局,只需要修改这条jmp的目标地址指向异常处理路径即可,减少Debug模式下增量编译的改动范围
- 方便断点插入:这种固定位置的占位指令可以让开发者直接在函数收尾位置插入断点,不需要动态修改指令偏移。
在Release模式开启优化后,这类无实际作用的冗余jmp会被编译器直接消除,不会出现在最终执行的代码中。
内容的提问来源于stack exchange,提问作者David

