Return in Memory & URVO & NRVO
本篇博客代码示例来自:https://sigcpp.github.io/2020/06/08/return-value-optimization#9
函数返回的两种方式
GCC 中函数返回值有两种传递方式:寄存器返回和内存返回。
寄存器返回:适用于标量类型(int、double、指针等)以及较小的聚合类型,直接通过寄存器(如 RAX、XMM0)传回。
内存返回:对于较大的聚合类型或非平凡类型,调用方(caller)会在栈上分配空间,并将该地址作为隐藏参数传递给被调方(callee)。被调方直接将返回值构造在这个地址上,同时 ABI 规定函数返回时将该地址存入指定的返回寄存器(如 RAX)。
内存返回的决策条件
编译器通过以下两个关键条件决定是否采用内存返回(代码位于 function.cc 和 i386.cc):
1. TREE_ADDRESSABLE(function.cc:2112)
当类型被标记为 TREE_ADDRESSABLE 时,强制使用内存返回:
- 非平凡可复制类型(non-trivially copyable)
- 带有虚函数的类(需要 vptr)
- 包含引用成员的结构体
即使只有 1 字节,也必须内存返回。
2. 平台大小限制(i386.cc:4459 / 4439)
x86-64 System V ABI:
- 整数类:需要超过 4 个整数寄存器(>32 字节)→ 内存返回
- SSE 类:需要超过 8 个 SSE 寄存器(>128 字节)→ 内存返回
- 实际按寄存器数量判断(通过
examine_argument)
x86-64 Windows ABI:
- 普通聚合类型:大小不在 {1, 2, 4, 8} 字节范围内 → 内存返回
- 例外:
__m128等 16 字节向量类型可通过XMM0寄存器返回
最常见触发内存返回的情况:返回非平凡类型或大小超出平台限制的聚合体。
拷贝消除优化:URVO 与 NRVO
URVO(Unnamed Return Value Optimization)和 NRVO(Named Return Value Optimization)都是拷贝消除(Copy Elision)技术,区别在于:
- URVO:针对匿名临时对象(如
return S();) - NRVO:针对具名局部变量(如
return s;)
URVO(匿名返回值优化)
原理:直接在隐藏指针指向的返回值槽中构造临时对象,省略从临时对象到返回值槽的拷贝。
Without URVO(无优化)
1 | S get_B() { |
总计:1 次默认构造 + 2 次拷贝构造 + 2 次析构
With URVO(有优化)
1 | S get_B() { |
总计:1 次默认构造 + 1 次析构
NRVO(具名返回值优化)
原理:将函数内的具名局部变量直接映射到返回值槽(通过隐藏指针),在该位置直接构造对象。
关键限制:NRVO 只有在单一路径返回同一局部变量时才能生效。
为什么多路径返回不同变量无法 NRVO?
- NRVO 的本质是将局部变量地址与返回值槽地址合二为一
- 不同局部变量在栈上位置不同,但返回值槽只有一个(由隐藏指针传入的固定地址)
- 编译器无法在编译时确定运行时哪个变量会被返回,因此无法预先建立映射关系
Without NRVO(无优化)
1 | S get_C() { |
总计:1 次默认构造 + 2 次拷贝构造 + 2 次析构
With NRVO(有优化)
1 | S get_C() { |
总计:1 次默认构造 + 1 次析构
总结
| 特性 | URVO | NRVO |
|---|---|---|
| 适用对象 | 匿名临时对象(return S();) |
具名局部变量(return s;) |
| 路径要求 | 无限制 | 必须单一路径返回同一变量 |
| 优化时机 | 编译期确定 | 编译期确定(需 SSA 分析) |
| C++ 标准 | 强制要求(C++17 起) | 可选优化 |
从 GCC 源码角度看,URVO 在语法层面即可判断,而 NRVO 需要在 check_return_expr 中检查所有返回路径是否指向同一变量(current_function_return_value 的跟踪逻辑),并在 finalize_nrv 中建立变量到返回值槽的映射关系。







