浅谈RVO与NRVO

2024-07-27 10:18:33 浏览数 (2)

RVO 和 NRVO

RVO(Return Value Optimization,返回值优化)和 NRVO(Named Return Value Optimization,命名返回值优化)是编译器进行的优化技术,旨在减少函数返回值的拷贝或移动操作。它们是 C 编译器在某些情况下自动应用的优化策略。

无优化

如果没有返回值优化(RVO)或命名返回值优化(NRVO),那么一个函数返回临时对象的一般步骤如下:

  1. 在函数内部创建临时对象。
  2. 在函数返回之前,分配内存来存储函数的返回值。
  3. 将临时对象拷贝或移动到返回值的内存位置。
  4. 函数返回,将返回值传递给调用方。

下面是一个示例,演示了在没有返回值优化的情况下,函数返回临时对象的步骤:

代码语言:javascript复制
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 允许编译器省略创建和销毁临时对象的过程,而是直接在接收对象的位置构造返回值。这样可以避免不必要的拷贝开销。例如:

代码语言:javascript复制
std::string createString()
{
    return "Hello, World!";  // 返回一个临时对象
}

在上面的例子中,RVO 允许编译器直接在函数内部构造目标位置的 std::string对象,而不是通过拷贝构造临时对象。这样可以减少不必要的拷贝开销。

  • 当编译器确定可以进行 RVO 时,它会:
    1. 在调用者的栈帧上为返回值分配空间,而不是在被调用函数的栈帧上。
    2. 将返回值对象的地址传递给被调用的函数,这样被调用的函数就可以直接在该地址上构造对象。
    3. 允许函数直接在预分配的内存位置构造返回值,从而避免了额外的拷贝构造和析构调用。

NRVO

NRVO 与 RVO 类似,但适用于返回函数内部已命名的局部变量。编译器优化这个过程,允许在调用者的栈帧上直接构造局部变量,避免了将局部变量拷贝到返回值的过程。这样也可以避免不必要的拷贝开销。例如:

代码语言:javascript复制
std::vector<int> createVector()
{
    std::vector<int> v{1, 2, 3, 4, 5};
    return v;  // NRVO将避免拷贝构造局部变量
}

在上面的例子中,NRVO 允许编译器直接在函数内部构造目标位置的 std::vector<int>对象,而不是通过拷贝构造局部变量。这样可以减少不必要的拷贝开销。

  • 在应用 NRVO 时,编译器会:
    1. 识别函数中将被返回的命名局部变量。
    2. 在调用者的栈帧上为该局部变量预留空间。
    3. 直接在该空间上构造局部变量,当函数返回时不需要移动或拷贝对象。

std::move 与优化技术的冲突

在返回局部变量时使用 std::move 时,将该局部变量转换为右值。这会阻止编译器对该局部变量进行优化,因为编译器无法确定该右值是否会被修改或继续使用,因此不能在原地构造返回值。

当使用 std::move 明确地将返回的对象转换为右值时,会改变编译器对该对象生命周期的理解。这是因为 std::move 表示一个意图,即表示该对象将不再被当前作用域使用,其资源可以被“移动”到另一个对象。由于 std::move 强制将对象视为右值,编译器必须假设该对象的资源(例如动态分配的内存)可能已经或即将被外部引用(例如,被移动到另一个对象)。

在这种情况下,编译器不能安全地在调用者的上下文中直接构造返回值。这是因为编译器不能确定在构造和移动操作之间对象的状态。如果编译器选择在原地构造对象,这可能违反 std::move 的语义,因为它意味着对象资源的所有权可能仍然在函数的作用域内。为了遵守 std::move 指示的移动语义,编译器将避免在调用者的上下文中直接构造对象,而是选择显式地执行移动构造或移动赋值操作。

代码语言:javascript复制
#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),使得编译器无法直接在函数内部构造目标位置的对象。因此,编译器将执行移动操作,将临时对象移动到返回值的位置,导致不必要的移动操作。

------本页内容已结束,喜欢请分享------

0 人点赞