为什么C++编译器不会跨函数调用转发const变量的加载?
bar函数做优化? 先看示例代码:
void black_box(int* foo); int foo(int x) { black_box(&x); return x; } int bar(const int x) { black_box(const_cast<int*>(&x)); return x; }
根据cppreference的定义:
const 对象指的是:
- 类型被const限定的对象,或者
- [...]
这类对象不可被修改:尝试直接或间接修改(例如通过非const类型的引用或指针修改const对象)会导致未定义行为。
bar函数中的x被声明为const,照理编译器可以假设black_box不会修改它,但GCC和Clang为foo和bar生成的x86汇编完全相同:
push rax mov dword ptr [rsp + 4], edi lea rdi, [rsp + 4] call black_box(int*)@PLT mov eax, dword ptr [rsp + 4] pop rcx ret
问题:为什么编译器不给bar函数做优化——把x移入被调用者保存寄存器,省去函数调用后的内存加载操作?
核心原因解析
形参的
const不等同于const对象
虽然bar的形参x带const限定,但它本质是函数形参,不属于标准定义的“const对象”范畴。const对象通常指全局/静态const变量、用const修饰且未被取地址/绑定引用的局部变量;而函数形参即便加了const,只要其地址被传递给外部函数,编译器就不能默认它不会被修改——哪怕这种修改属于未定义行为,编译器也不会以此为依据做激进优化。调用约定的硬性限制
x86-64的System V调用约定中,整数参数通过rdi传递,但rdi是调用者保存寄存器,被调用函数black_box有权修改这个寄存器的值。如果编译器把x留在rdi里,调用black_box后寄存器的值可能被破坏,因此必须先把x存入栈中,调用结束后再从栈里读取返回值——这也是foo和bar汇编完全一致的直接原因。编译器的优化保守性
编译器不会主动检测用户代码中的未定义行为来做优化。const_cast修改const形参属于UB,但编译器只会基于合法代码的语义做优化,不会假设用户不会写出UB代码,因此不会跳过栈加载的步骤。
内容的提问来源于stack exchange,提问作者ChrisB

