C-返回值优化Return-Value-Optimization
C++ 返回值优化(Return Value Optimization)
Intro
返回值优化(Return Value Optimization, RVO)是 C++中的一种编译器优化技术, 它允许编译器在某些情况下省略临时对象的创建和复制/移动操作, 从而提高程序性能. RVO 主要应用于函数返回值的场景.
两种形式的 RVO
假定我们有这样一个类:
class MyClass {
std::string name_;
public:
void SetName(std::string name) { name_ = name; }
MyClass() { fmt::println("默认构造函数"); }
MyClass(std::string name) : name_(name) {
fmt::println("构造函数: {}", name);
}
MyClass(const MyClass& rhs) : name_(rhs.name_) {
fmt::println("拷贝构造函数: {}", rhs.name_);
}
MyClass(MyClass&& rhs) noexcept {
name_ = std::move(rhs.name_);
rhs.name_ = name_ + "[MOVED]";
fmt::println("移动构造函数: {}", name_);
}
MyClass& operator=(const MyClass& rhs) {
fmt::println("拷贝赋值运算符");
return *this;
}
MyClass& operator=(MyClass&&) noexcept {
fmt::println("移动赋值运算符");
return *this;
}
~MyClass() { fmt::println("析构函数, name={}", name_); }
};
- 命名返回值优化(Named Return Value Optimization, NRVO)
- 当一个函数返回一个局部变量时, 如果这个变量的类型与函数返回类型相同或可转换, NRVO 允许编译器直接在调用者的作用域内构造该局部变量, 而不是先构造然后复制到返回值.
MyClass NamedRVO(bool useFirst) { MyClass result("named RVO"); return result; }
- 无名返回值优化(Unamed Return Value Optimization)
- 当返回一个临时对象时, 编译器可以在调用者的空间直接构造这个临时对象, 避免了临时对象的生成以及后续的复制/移动操作.
MyClass UnamedRVO() { return MyClass("unamed RVO"); }
我们使用如下的测试代码, 有兴趣的读者可以打开运行(CSDN不适用, 可以访问 ):
int main(int n, char** args) {
MyClass unamed = UnamedRVO();
fmt::println("=======");
MyClass named = NamedRVO(true);
}
输出:
构造函数
=======
默认构造函数
析构函数, name=named RVO
析构函数, name=unamed RVO
从析构函数的调用次数我们可以判断出使用了 RVO, 因为没有临时变量的产生. 直接在返回值所在的地方生成对象, 省略了返回值的拷贝或者移动.
为了对比, 我们再看一下没有启用 RVO 的输出:
构造函数: unamed RVO
移动构造函数: unamed RVO
析构函数, name=unamed RVO[moved]
移动构造函数: unamed RVO
析构函数, name=unamed RVO[moved]
=======
构造函数: named RVO
移动构造函数: named RVO
析构函数, name=named RVO[moved]
移动构造函数: named RVO
析构函数, name=named RVO[moved]
析构函数, name=named RVO
析构函数, name=unamed RVO
当启用了 RVO 后, 我们看到程序实际上是在返回值所在的地方构造了一个对象, 不需要借助拷贝或者移动.
RVO 的发展历程
- 从 C++98 开始, 编译器被允许做 RVO 优化
- 从 C++7 开始, 编译器被强制要求做 RVO 优化(Mandatory Copy Elision)
RVO 可以被禁用, 在编译的时候指定
-fno-elide-constructors
(GCC/Clang) 来禁用 RVO.
C++17 中的改进
从 C++17 开始, 复制省略成为了标准的一部分, 这意味着即使类的复制/移动构造函数有副作用(如打印信息), 编译器也允许跳过这些步骤, 直接构造返回的对象. 这使得 RVO 不仅是一个优化选项, 而且是语言的一个特性, 进一步提高了代码的效率和简洁性.
RVO 失效的情况
下面的情况下 RVO 不会被触发.
编译器选项设置了
-fno-elide-constructors
函数返回的是一个全局变量:
MyClass global("global"); MyClass NoRVO1() { return global; }
当返回类型不匹配时:
class Child : public MyClass { public: Child() : MyClass("child") { fmt::println("child"); } }; MyClass NoRVO2() { return Child(); }
如果返回的可能是不同的对象, 那么编译器将无法确定哪个对象应该被返回, 因此无法触发 RVO.
多个
return
语句MyClass NoRVO3(int x) { MyClass r1("r1"); MyClass r2("r2"); if (x > 0) { return r2; } return r1; }
或者单个 return 语句里面有条件分支
MyClass NoRVO4(int x) { MyClass r1("r1"); MyClass r2("r2"); return x > 0 ? r2 : r1; }
加了一个不必要的
std::move
. 这属于画蛇添足了, RVO 比起 move 来更高效.MyClass NoRVO5() { MyClass r1("r1"); return std::move(r1); }
RVO 与 std::move
上面讲到
std::move
会导致 RVO 失效, 那么或许有人会问: 已经存在 move 了那 RVO 还有必要吗?
实际上是有必要的. 因为 RVO 是在返回位置处之间创建对象, 而 move 是先创建一个临时变量, 再进行 move. 明显多做了一步, 这一步无论再小也是代价. 另外对于 POD 类型来说, move 就是拷贝.
下面做了一个测试对比, 我们看看 move 和 RVO 的性能差别:
#include <benchmark/benchmark.h>
#include <fmt/core.h>
#include <string>
//{
class SimpleClass {
std::string name_;
public:
SimpleClass(std::string name) : name_(name) {}
};
SimpleClass UnamedRVO() { return SimpleClass("test string"); }
SimpleClass Move() { return std::move(SimpleClass("test string")); }
void BM_UnamedRVO(benchmark::State& state) {
for (auto _ : state) {
SimpleClass unamed = UnamedRVO();
benchmark::DoNotOptimize(unamed);
}
}
void BM_Move(benchmark::State& state) {
for (auto _ : state) {
SimpleClass moved = Move();
benchmark::DoNotOptimize(moved);
}
}
BENCHMARK(BM_UnamedRVO);
BENCHMARK(BM_Move);
BENCHMARK_MAIN();
//}
测试结果(Release 版本):
-------------------------------------------------------
Benchmark Time CPU Iterations
-------------------------------------------------------
BM_UnamedRVO 5.25 ns 5.24 ns 116932087
BM_Move 10.7 ns 10.6 ns 68695102
请注意在使用 benchmark 库的时候, 需要使用
benchmark::DoNotOptimize
来避免编译器优化掉代码. 因为
unamed
和
moved
都是局部变量, 编译器可能会优化掉它们的创建和销毁. 就会出现运行开销为
0
的谬误.
-------------------------------------------------------
Benchmark Time CPU Iterations
-------------------------------------------------------
BM_UnamedRVO 0.000 ns 0.000 ns 1000000000000
BM_Move 10.1 ns 10.1 ns 66627774