本篇博客代码示例来自: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
2
3
4
5
6
7
8
S get_B() {
return S(); // 1. 在函数内默认构造临时对象
} // 2. 拷贝构造到返回值槽(copy ctor)
// 3. 析构临时对象

int main() {
S s = get_B(); // 4. 用返回值初始化 s(可能还有拷贝)
} // 5. 析构 s

总计:1 次默认构造 + 2 次拷贝构造 + 2 次析构

With URVO(有优化)

1
2
3
4
5
6
7
S get_B() {
return S(); // 1. 直接在返回值槽中默认构造
}

int main() {
S s = get_B(); // s 直接引用返回值槽中的对象
} // 2. 析构 s

总计:1 次默认构造 + 1 次析构


NRVO(具名返回值优化)

原理:将函数内的具名局部变量直接映射到返回值槽(通过隐藏指针),在该位置直接构造对象。

关键限制:NRVO 只有在单一路径返回同一局部变量时才能生效。

为什么多路径返回不同变量无法 NRVO?

  • NRVO 的本质是将局部变量地址与返回值槽地址合二为一
  • 不同局部变量在栈上位置不同,但返回值槽只有一个(由隐藏指针传入的固定地址)
  • 编译器无法在编译时确定运行时哪个变量会被返回,因此无法预先建立映射关系

Without NRVO(无优化)

1
2
3
4
5
6
7
8
9
S get_C() {
S s; // 1. 默认构造局部变量 s
s.i = 8;
return s; // 2. 拷贝构造到返回值槽(copy ctor)
} // 3. 析构局部变量 s

int main() {
S s = get_C(); // 4. 可能还有一次拷贝(视情况而定)
} // 5. 析构 s

总计:1 次默认构造 + 2 次拷贝构造 + 2 次析构

With NRVO(有优化)

1
2
3
4
5
6
7
8
9
S get_C() {
S s; // 1. 直接在返回值槽中构造 s(s 的地址就是返回值槽地址)
s.i = 8;
return s; // 无拷贝,直接返回
}

int main() {
S s = get_C(); // s 直接引用返回值槽中的对象
} // 2. 析构 s

总计:1 次默认构造 + 1 次析构


总结

特性 URVO NRVO
适用对象 匿名临时对象(return S();) 具名局部变量(return s;)
路径要求 无限制 必须单一路径返回同一变量
优化时机 编译期确定 编译期确定(需 SSA 分析)
C++ 标准 强制要求(C++17 起) 可选优化

从 GCC 源码角度看,URVO 在语法层面即可判断,而 NRVO 需要在 check_return_expr 中检查所有返回路径是否指向同一变量(current_function_return_value 的跟踪逻辑),并在 finalize_nrv 中建立变量到返回值槽的映射关系。