C++ 踩坑: 返回值优化 (RVO)
返回值优化 (Return Value Optimization, RVO) 是编译器一种 抑制拷贝(Copy Elision) 的优化机制, 避免代码发生不必要的拷贝. 特别对于返回一些局部创建的大对象来说, 有助于提高性能. 虽然这是编译器的行为, 但是并非所有情况下, 编译器都会对返回值进行优化. 因此, 开发者需要搞清楚何种情况, 才会触发此机制.
返回值优化, 包括 RVO 和 NRVO (Named RVO). 一般未具体说明, 前者包含后者. 为方便于观察对象的构造过程, 这里提供了一个基础类 (Rule of Three, 无移动构造函数和移动赋值操作符).
static int counter; // counter to identify instances
struct Data {
int i{ 0 };
int id;
Data() : id{ ++counter } {
std::cout << "ctor " << id << "\n";
}
Data(const Data& s) : i{ s.i }, id{ ++counter } {
std::cout << "copy ctor " << id << "\n";
}
Data& operator=(const Data& data) {
i = data.i;
std::cout << "copy assign " << data.id << " to " << id << "\n";
return *this;
}
~Data() {
std::cout << "dtor " << id << "\n";
}
};
不具名返回值优化 URVO
不具名返回值优化发生在返回一个无名对象或者临时对象, 一般是 Return
语句中直接创建并返回的对象.
URVO 从 C++98 Section 12.2 of that standard 开始已被许可, 但是一直到 C++17 编译器才强制返回值优化. 为了观察是否发生返回值优化, 无优化的例子使用 C++14 编译 (编译器版本为: x86-64 gcc 12.2
, 由于编译器默认开启该优化选项, 因此需要在编译时关闭该选项), 编译选项如下:
标题 | 编译选项 |
---|---|
无优化 C++11/14 | -Wall -Wextra -pedantic -fno-elide-constructors |
优化 C++17/20 | -Wall -Wextra -pedantic |
代码示例 1
Data GetData() {
return Data{};
}
int main() {
Data d = GetData();
return 0;
}
可以看出, 在 RVO 时, 对象只被构造了一次. 而未 RVO 时, 对象则发生了多次拷贝 (注 [坑, 待填]: 第三次拷贝构造和赋值右值相关, 在 特殊成员函数 系列中有提及, 如果类遵循 Rule of Big Five
, 这里不会发生拷贝构造, 而是移动赋值).
-
无优化 (运行代码)
Data GetData() { return Data{}; // ctor 1 } // copy ctor 2, dtor 1 int main() { Data d = GetData(); // copy ctor 3, dtor 2 return 0; } // dtor 3
-
优化后 (运行代码)
Data GetData() { return Data{}; // ctor 1 } int main() { Data d = GetData(); return 0; } // dtor 1
具名返回值优化 RVO
具名返回值优化一般发生在返回一个已经创建的对象.
GCC 默认开启具名返回值优化, 包括 C++98. 因此编译时需显式禁用选项, 编译选项如下:
标题 | 编译选项 |
---|---|
无优化 C++11/C++14 | -Wall -Wextra -pedantic -fno-elide-constructors |
无优化 C++17/20 | -Wall -Wextra -pedantic -fno-elide-constructors |
优化 C++11/14/17/20 | -Wall -Wextra -pedantic |
此外, 需要注意的是, MSVC 默认是不开启具名返回值优化的, 若要开启优化, 需要在 /O2
下编译.
代码示例 2
Data GetData() {
Data d;
d.i = 999;
return d;
}
int main() {
Data d = GetData();
return 0;
}
输出
-
无优化 C++11/C++14, (运行代码)
Data GetData() { return Data{}; // ctor 1 } // copy ctor 2, dtor 1 int main() { Data d = GetData(); // copy ctor 3, dtor 2 return 0; } // dtor 3
-
无优化 C++17/20, (运行代码)
Data GetData() { return Data{}; // ctor 1 } // copy ctor 2, dtor 1 int main() { Data d = GetData(); return 0; } // dtor 2
-
优化 C++11/14/17/20 (运行代码)
Data GetData() { return Data{}; // ctor 1 } int main() { Data d = GetData(); return 0; } // dtor 1
容器的返回值优化
对于容器来说, 若整个容器发生拷贝, 代价很高. 因此, 非常有必要考虑返回值优化. 一般来说, C++ 默认对容器生效.
std::vector<Data> GetDataContainers() {
//!!! case 1
// return std::vector<Data>{Data{}}; // ctor 1, copy ctor 2, dtor 1
//!!! case 2
std::vector<Data> vec{Data{}}; // ctor 1, copy ctor 2, dtor 1
return vec;
}
int main() {
auto d = GetDataContainers();
return 0;
} // dtor 2
从 C++17 开始, 编译器强制开启返回值优化后, 即使 -fno-elide-constructors
, 优化仍然发生.
ctor 1
copy ctor 2
dtor 1
dtor 2
在 C++14 及其之前版本中, 在 -fno-elide-constructors
下编译输出:
ctor 1
copy ctor 2
copy ctor 3
copy ctor 4
dtor 3
dtor 2
dtor 1
dtor 4
返回值优化的特殊情况
对于存在分支的函数, 若所有分支都返回同一个具名对象, 才会开启返回值优化. 编译选项: -std=c++20 -Wall -Wextra -pedantic
.
代码示例 3 - 所有分支返回同一具名对象 (优化)
若分支返回全是同一具名对象, 发生返回值优化. 运行代码
Data GetData(int param) {
Data d; // ctor 1
if (param % 2 == 0) {
d.i = 1;
return d;
}
else if (param % 2 == 1) {
d.i = 2;
return d;
}
return d;
}
int main() {
Data d = GetData(0);
return 0;
} // dtor 2
代码示例 4 - 所有分支返回非同一对象 (无优化)
若分支返回不全是同一具名对象, 则无返回值优化. 因为返回的对象在运行时确定, 编译器无法在编译期决定.
Data GetData(int param) {
Data d; // ctor 1
if (param % 2 == 0) {
d.i = 1;
return d;
}
else if (param % 2 == 1) {
Data d2;
d2.i = 2;
return d2;
}
return d;
} // copy ctor 2, dtor 1
int main() {
Data d = GetData(0);
return 0;
} // dtor 2
Data GetData(int param) {
if (param % 2 == 0) {
Data d1; // ctor 1
d1.i = 1;
return d1;
} else {
Data d2; // ctor 1
d2.i = 2;
return d2;
}
} // copy ctor 2, dtor 1
int main() {
Data d = GetData(0);
return 0;
} // dtor 2
Data GetData(int param) {
Data d1, d2; // ctor 1, ctor 2
if (param % 2 == 0) {
return d1;
} else {
return d2;
}
} // copy ctor 3, dtor 2, dtor 1
int main() {
Data d = GetData(0);
return 0;
} // dtor 3
代码示例 5 - 函数返回结果用于赋值 (无优化)
需要注意的另一种情况是, 如果调用函数时, 造成的是拷贝赋值, 而不是拷贝构造, 即使是不具名的情况, 也不会发生返回值优化 (注: 换个思路理解, 编译器不清楚赋值左侧的值从创建到赋值之间, 将处于何种状态, 或者进行何种操作, 所以不会对这种形式做返回值优化. 为避免这种情况的拷贝赋值, 可以通过移动赋值来消除).
Data GetData(int param) {
//!!! sub case 1
// Data d{}; // ctor 2
// return d;
//!!! sub case 2
// return Data{}; // ctor 2
//!!! sub case 3
Data d{}; // ctor 2
if (param % 2 == 0) {
d.i = 1;
return d;
} else {
d.i = 2;
return d;
}
}
int main() {
Data d; // ctor 1
d = GetData(0); // copy assign 2 to 1
return 0;
} // dtor 2, dtor1
代码示例 6 - 函数返回成员对象 (无优化)
函数返回的是局部对象的成员变量, 也无法作用返回值优化, 即使是匿名变量.
struct DataWrap {
Data d;
};
Data GetData() {
return DataWrap{}.d; // ctor 1
} // copy ctor 2, dtor 1
int main() {
Data d = GetData();
return 0;
} // dtor 2
代码示例 7 - 函数返回参数或者全局变量 (无优化)
函数返回的是输入参数或者全局变量, 也无返回值优化.
Data GetData(Data d) {
return d;
} // copy ctor 3
int main() {
Data d1{}; // ctor 1
Data d2 = GetData(d1); // copy ctor 2
return 0;
} // dtor 2, dtor 3, dtor2
Data sd{}; // ctor 1
Data GetData() {
return sd;
} // copy ctor 2, dtor 1
int main() {
Data d = GetData();
return 0;
} // dtor 2
代码示例 8 - 由 std::move
返回 (无优化)
通过显式调用 std::move
返回函数结果往往是错误的. 即使如此, 这试图使对象显式调用移动构造函数, 导致返回值优化被抑制.
Data GetData() {
Data d; // ctor 1
return std::move(d);
} // copy ctor 2, dtor 1
int main() {
Data d = GetData();
return 0;
} // dtor 2
异常 (try-catch) 中的返回
[待填]