RVO 和 NRVO
RVO(Return Value Optimization,返回值优化)和 NRVO(Named Return Value Optimization,命名返回值优化)是编译器进行的优化技术,旨在减少函数返回值的拷贝或移动操作。它们是 C++编译器在某些情况下自动应用的优化策略。
无优化
如果没有返回值优化(RVO)或命名返回值优化(NRVO),那么一个函数返回临时对象的一般步骤如下:
- 在函数内部创建临时对象。
- 在函数返回之前,分配内存来存储函数的返回值。
- 将临时对象拷贝或移动到返回值的内存位置。
- 函数返回,将返回值传递给调用方。
下面是一个示例,演示了在没有返回值优化的情况下,函数返回临时对象的步骤:
std::string createString()
{
std::string str = "Hello, World!";
return str; // 返回临时对象
}
int main()
{
std::string result = createString(); // 接收返回值的对象
std::cout << result << std::endl;
return 0;
}
在上面的例子中,createString
函数创建了一个临时的 std::string
对象 str
,然后在函数返回之前,将 str
拷贝或移动到返回值的内存位置。在 main
函数中,返回值被拷贝构造到名为 result
的对象中。
如果没有返回值优化,这个过程将涉及临时对象的构造、拷贝或移动和析构。但是,通过返回值优化,编译器可以在函数内部直接构造目标位置的对象,避免了不必要的拷贝或移动操作,从而提高了性能。
RVO
RVO 是一种编译器优化技术,它避免了从函数返回时创建临时对象。当函数返回一个临时对象(通常是由构造函数直接初始化的匿名对象)时,RVO 允许编译器省略创建和销毁临时对象的过程,而是直接在接收对象的位置构造返回值。这样可以避免不必要的拷贝开销。例如:
std::string createString()
{
return "Hello, World!"; // 返回一个临时对象
}
在上面的例子中,RVO 允许编译器直接在函数内部构造目标位置的 std::string
对象,而不是通过拷贝构造临时对象。这样可以减少不必要的拷贝开销。
- 当编译器确定可以进行 RVO 时,它会:
- 在调用者的栈帧上为返回值分配空间,而不是在被调用函数的栈帧上。
- 将返回值对象的地址传递给被调用的函数,这样被调用的函数就可以直接在该地址上构造对象。
- 允许函数直接在预分配的内存位置构造返回值,从而避免了额外的拷贝构造和析构调用。
NRVO
NRVO 与 RVO 类似,但适用于返回函数内部已命名的局部变量。编译器优化这个过程,允许在调用者的栈帧上直接构造局部变量,避免了将局部变量拷贝到返回值的过程。这样也可以避免不必要的拷贝开销。例如:
std::vector<int> createVector()
{
std::vector<int> v{1, 2, 3, 4, 5};
return v; // NRVO将避免拷贝构造局部变量
}
在上面的例子中,NRVO 允许编译器直接在函数内部构造目标位置的 std::vector<int>
对象,而不是通过拷贝构造局部变量。这样可以减少不必要的拷贝开销。
- 在应用 NRVO 时,编译器会:
- 识别函数中将被返回的命名局部变量。
- 在调用者的栈帧上为该局部变量预留空间。
- 直接在该空间上构造局部变量,当函数返回时不需要移动或拷贝对象。
std::move 与优化技术的冲突
在返回局部变量时使用 std::move 时,将该局部变量转换为右值。这会阻止编译器对该局部变量进行优化,因为编译器无法确定该右值是否会被修改或继续使用,因此不能在原地构造返回值。
当使用 std::move
明确地将返回的对象转换为右值时,会改变编译器对该对象生命周期的理解。这是因为 std::move
表示一个意图,即表示该对象将不再被当前作用域使用,其资源可以被“移动”到另一个对象。由于 std::move
强制将对象视为右值,编译器必须假设该对象的资源(例如动态分配的内存)可能已经或即将被外部引用(例如,被移动到另一个对象)。
在这种情况下,编译器不能安全地在调用者的上下文中直接构造返回值。这是因为编译器不能确定在构造和移动操作之间对象的状态。如果编译器选择在原地构造对象,这可能违反 std::move
的语义,因为它意味着对象资源的所有权可能仍然在函数的作用域内。为了遵守 std::move
指示的移动语义,编译器将避免在调用者的上下文中直接构造对象,而是选择显式地执行移动构造或移动赋值操作。
#include <iostream>
#include <vector>
std::vector<int> createVector()
{
std::vector<int> v{1, 2, 3, 4, 5};
return std::move(v); // 使用std::move返回局部变量
}
int main()
{
std::vector<int> result = createVector(); // 接收返回值的对象
// 使用返回值
return 0;
}
在上述示例中,createVector 函数返回一个局部变量 v,使用 std::move 将其转换为右值。这将阻止编译器应用命名返回值优化(NRVO),使得编译器无法直接在函数内部构造目标位置的对象。因此,编译器将执行移动操作,将临时对象移动到返回值的位置,导致不必要的移动操作。