返回值优化(RVONRVO)详解

一、什么是返回值优化(RVO/NRVO/URVO)?

返回值优化(Return Value Optimization, RVO)

命名返回值优化(Named Return Value Optimization, NRVO)

未命名返回值优化(Unnamed Return Value Optimization, NRVO)

Since C++17, URVO is mandatory and no longer considered a form of copy elision.

https://en.cppreference.com/w/cpp/language/copy_elision.html

是编译器在特定情况下对函数返回值的优化技术,旨在减少不必要的对象拷贝或移动操作,从而提高程序性能。那究竟什么是返回值优化呢?其根本就是拷贝省略。

  • RVO:当函数返回一个局部对象时,编译器直接在调用者的栈帧中构造该对象,避免在函数返回时进行拷贝或移动。
  • NRVO:当函数返回一个命名的局部对象时,编译器同样直接在调用者的栈帧中构造该对象,避免拷贝或移动。
  • URVO:当函数返回一个未命名的临时对象时,编译器同样直接在调用者的栈帧中构造该对象,避免拷贝或移动

二、为什么需要 RVO?

在 C++ 中,函数返回对象时,通常会经历以下步骤:

  1. 在函数内部构造一个临时对象。
  2. 将临时对象拷贝或移动到调用者的栈帧中。
  3. 临时对象被销毁。

这种额外的拷贝或移动操作会增加程序的开销,尤其是在对象较大或构造、析构成本较高时。RVO通过消除这些不必要的操作来优化性能。总结就是RVO比移动语义更高效(直接构造,无额外操作)。

三、RVO代码分析

编译器通过以下方式实现NRVO/URVO:

  1. 直接构造:编译器在调用者的栈帧中直接构造返回对象,而不是在函数内部构造临时对象。
  2. 跳过拷贝或移动:由于对象已经在调用者的栈帧中构造,因此不需要进行拷贝或移动操作。

CMakeLists.txt

cmake_minimum_required(VERSION 3.31)
project(untitled)

#set(CMAKE_CXX_STANDARD 11)
#set(CMAKE_CXX_STANDARD 14)
#set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD 20)

## 打印当前使用的编译器信息
#message(STATUS "C compiler ID: ${CMAKE_C_COMPILER_ID}")
#message(STATUS "C++ compiler ID: ${CMAKE_CXX_COMPILER_ID}")
#
#if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
#    add_compile_options(-fno-elide-constructors)
#    message(STATUS "GNU")
#elseif (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
#    add_compile_options(/Od)
#    message(STATUS "MSVC")
#else ()
#    message(WARNING "Unknown Compiler!")
#endif ()

# MSVC 和 MinGW (GCC) 禁用RVO优化
if (MSVC)
    add_compile_options(/Od)
elseif (MINGW)
    # MinGW (GCC): 禁用RVO优化
    add_compile_options(-fno-elide-constructors)
else ()
    message(WARNING "Unknown Compiler!")
endif ()

add_executable(untitled main.cpp)

main.cpp

#include <iostream>

class MyClass {
public:
    MyClass() {
        std::cout << "Default Constructor" << std::endl;
    }

    MyClass(const MyClass &) {
        std::cout << "Copy Constructor" << std::endl;
    }

    MyClass(MyClass &&) noexcept {
        std::cout << "Move Constructor" << std::endl;
    }

    ~MyClass() {
        std::cout << "Destructor" << std::endl;
    }
};

// 返回一个命名的局部对象(可能触发NRVO)
MyClass createWithNRVO() {
    MyClass obj; // 命名局部对象
    return obj; // 直接在调用者的栈帧中构造
}

// 返回一个未命名的临时对象(可能触发URVO)
MyClass createWithURVO() {
    return {}; // 直接构造临时对象并返回
    // return MyClass();
}

int main() {
    std::cout << "NRVO:" << std::endl;
    MyClass b = createWithNRVO();

    std::cout << "URVO:" << std::endl;
    MyClass a = createWithURVO();

    return 0;
}

当RVO触发时(通常在优化模式下):

NRVO:
Default Constructor
URVO:
Default Constructor
Destructor
Destructor

当RVO未触发时(使用-fno-elide-constructors编译):

NRVO:
Default Constructor
Move Constructor
Destructor
URVO:
Default Constructor
Destructor
Destructor

编译选项

要观察RVO的效果,也可以使用以下编译命令:

# 启用RVO(默认情况下大多数编译器会启用)
g++ -std=c++11 -O2 main.cpp -o untitled
 
# 禁用RVO以观察拷贝、移动操作
g++ -std=c++11 -O0 -fno-elide-constructors main.cpp -o untitled

四、NRVO/URVO 的触发条件

编译器是否应用 NRVO/URVO 取决于以下因素:

  1. 编译器实现:不同编译器(如 GCC、Clang、MSVC)对 NRVO/URVO 的支持程度不同。
  2. 优化级别:通常需要在 -O1 或更高优化级别下启用。
  3. 代码结构
    • 返回的必须是局部对象(匿名或命名)。
    • 不能有多个返回路径(如 if-else 返回不同对象)。
    • 不能有异常处理干扰(如 try-catch)。

五、人为阻止 RVO

使用 std::move 谨慎,在返回局部对象时,显式使用 std::move(obj) 会阻止 NRVO(但可能触发移动语义)。不同编译器阻止RVO的范围也不同(MSVC有点特殊)。

  • GCC编译器仅std::move()的对象会阻止NRVO
  • MSVC编译器C++17及之前版本会影响全局阻止RVO,C++20恢复只影响局部对象NRVO
#include <iostream>

class MyClass {
public:
    MyClass() {
        std::cout << "Default Constructor" << std::endl;
    }

    MyClass(const MyClass &) {
        std::cout << "Copy Constructor" << std::endl;
    }

    MyClass(MyClass &&) noexcept {
        std::cout << "Move Constructor" << std::endl;
    }

    ~MyClass() {
        std::cout << "Destructor" << std::endl;
    }
};

// 演示 NRVO(无 std::move)
MyClass createObjectNRVO() {
    MyClass obj; // 局部对象
    return obj; // 可能触发 NRVO
}

// 显式使用 std::move(阻止 NRVO,触发移动语义)
MyClass createObjectWithMove() {
    MyClass obj;
    return std::move(obj); // 强制移动语义,阻止 NRVO
}

int main() {
    MyClass a = createObjectNRVO(); // 可能触发 NRVO(无拷贝/移动)
    MyClass b = createObjectWithMove(); // 触发移动构造
    return 0;
}

六、禁用RVO

微软官网MSVC编译器优化选项

https://learn.microsoft.com/en-us/cpp/build/reference/compiler-options-listed-by-category?view=msvc-170

  • MSVC 没有提供像 -fno-elide-constructors 这样的选项来禁用 RVO。
  • MSVC 会在 C++20 及以后版本强制执行 RVO,无法通过编译选项关闭。

可行的替代方案:

如果你目的是测试或研究对象的拷贝/移动行为,可以:

add_compile_options(/Od)

禁用规则总结:

  • NRVO
    • GCC、MSVC 可以选择禁用,不受版本影响
  • URVO
    • MSVC C++11 强制开启,无法禁用
    • GCC C++17 强制开启,无法禁用
  • MSVC 会在 C++20 及以后版本强制执行 RVO (NRVO/URVO),无法通过编译选项关闭。

GCC

C++11

NRVO:
Default Constructor
Move Constructor
Destructor
Move Constructor
Destructor
URVO:
Default Constructor
Move Constructor
Destructor
Destructor
Destructor

C++14

NRVO:
Default Constructor
Move Constructor
Destructor
Move Constructor
Destructor
URVO:
Default Constructor
Move Constructor
Destructor
Destructor
Destructor

C++17

NRVO:
Default Constructor
Move Constructor
Destructor
URVO:
Default Constructor
Destructor
Destructor

C++20

NRVO:
Default Constructor
Move Constructor
Destructor
URVO:
Default Constructor
Destructor
Destructor

MSVC

C++11

NRVO:
Default Constructor
Move Constructor
Destructor
URVO:
Default Constructor
Destructor
Destructor

C++14

NRVO:
Default Constructor
Move Constructor
Destructor
URVO:
Default Constructor
Destructor
Destructor

C++17

NRVO:
Default Constructor
Move Constructor
Destructor
URVO:
Default Constructor
Destructor
Destructor

C++20

NRVO:
Default Constructor
URVO:
Default Constructor
Destructor
Destructor

Sync CMake Changes

Ctrl+Shift+O

七、如何验证 RVO 是否发生?

  • 通过观察构造函数和拷贝、移动构造函数的调用情况。
  • 使用编译器选项(如 GCC 的 -fdump-tree-optimized)查看优化后的中间代码。
C:\Program Files\JetBrains\CLion 2025.1\bin\mingw\bin

g++ -std=c++11 -fdump-tree-optimized -O0 main.cpp -o untitled
g++ -std=c++11 -fdump-tree-optimized -O2 main.cpp -o untitled

八、练习题

#include <iostream>

struct Noisy {
    Noisy() { std::cout << "constructed at " << this << std::endl; }
    Noisy(const Noisy &) { std::cout << "copy-constructed" << std::endl; }
    Noisy(Noisy &&) noexcept { std::cout << "move-constructed" << std::endl; }
    ~Noisy() { std::cout << "destructed at " << this << std::endl; }
};

Noisy f() {
    Noisy v = Noisy(); // (until C++17) copy elision initializing v from a temporary;
    // the move constructor may be called
    // (since C++17) "guaranteed copy elision"
    return v; // copy elision ("NRVO") from v to the result object;
    // the move constructor may be called
}

void g(Noisy arg) {
    std::cout << "&arg = " << &arg << std::endl;
}

int main() {
    Noisy v = f(); // (until C++17) copy elision initializing v from the result of f()
    // (since C++17) "guaranteed copy elision"

    std::cout << "&v = " << &v << std::endl;

    g(f()); // (until C++17) copy elision initializing arg from the result of f()
    // (since C++17) "guaranteed copy elision"
}
posted @ 2020-12-28 12:30  hellsino  阅读(55)  评论(0)    收藏  举报