当前位置: 首页 > news >正文

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_); }
};
  1. 命名返回值优化(Named Return Value Optimization, NRVO): 当一个函数返回一个局部变量时, 如果这个变量的类型与函数返回类型相同, NRVO 允许编译器直接在调用者的作用域内构造该局部变量, 而不是先构造然后复制到返回值.

    MyClass NamedRVO(bool useFirst) {
      MyClass result("named RVO");
      return result;
    }
    
  2. 无名返回值优化(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

RVO 的发展历程

  1. 从 C++98 开始, 编译器被允许做 RVO 优化
  2. 从 C++17 开始, 编译器被强制要求做 RVO 优化(Mandatory Copy Elision)

RVO 可以被禁用, 在编译的时候指定 -fno-elide-constructors(GCC/Clang) 来禁用 RVO.

C++17 中的改进

从 C++17 开始, 复制省略成为了标准的一部分, 这意味着即使类的复制/移动构造函数有副作用(如打印信息), 编译器也允许跳过这些步骤, 直接构造返回的对象. 这使得 RVO 不仅是一个优化选项, 而且是语言的一个特性, 进一步提高了代码的效率和简洁性.

RVO 失效的情况

下面的情况下 RVO 不会被触发.

  1. 编译器选项设置了 -fno-elide-constructors

  2. 函数返回的是一个全局变量:

    MyClass global("global");
    MyClass NoRVO1() { return global; }
    
  3. 当返回类型不匹配时:

    class Child : public MyClass {
    public:
      Child() : MyClass("child") { fmt::println("child"); }
    };
    
    MyClass NoRVO2() { return Child(); }
    
  4. 如果返回的可能是不同的对象, 那么编译器将无法确定哪个对象应该被返回, 因此无法触发 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;
    }
    
  5. 加了一个不必要的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来避免编译器优化掉代码. 因为unamedmoved都是局部变量, 编译器可能会优化掉它们的创建和销毁. 就会出现运行开销为 0 的谬误.

-------------------------------------------------------
Benchmark             Time             CPU   Iterations
-------------------------------------------------------
BM_UnamedRVO      0.000 ns        0.000 ns   1000000000000
BM_Move            10.1 ns         10.1 ns     66627774

参考链接

  • 演示示例源码
  • C++ RVO: Return Value Optimization for Performance in Bloomberg C++ Codebases - Michelle Fae D’Souza

相关文章:

  • 学习TensorFlow前的NumPy核心知识点
  • C++学习之二叉树
  • S32K144入门笔记(十五):ADC(转换器部分)的解读
  • windows安装Elasticsearch
  • 科普:为何要对特征进行分箱?
  • C++单例模式精解
  • OSG 和 VTK 在JS仿真中应用的更详细对比分析,包括它们的技术特点、适用场景、优缺点以及如何选择或结合使用
  • softmax回归遇到的训练集准确率小于测试集准确率的问题
  • 【MySQL】多表查询(笛卡尔积现象,联合查询、内连接、左外连接、右外连接、子查询)-通过练习快速掌握法
  • 使用 Docker 部署 MySQL 8
  • Kotlin知识体系(二) : Kotlin的七个关键特性
  • 如何在 Web Component 中优雅地使用 React
  • 代码随想录算法训练营第32天 | 509. 斐波那契数 70. 爬楼梯 746. 使用最小花费爬楼梯
  • Part1:基于国内源完成Kubernetes集群部署
  • 【从零开始学习计算机科学】算法分析(四)图 与 最大流算法
  • Qt-D指针与Q指针的设计哲学
  • 【数学建模】层次分析法(AHP)详解及其应用
  • PyTorch 生态概览:为什么选择动态计算图框架?
  • ffmpeg基础整理
  • 学校IPTV数字电视直播:IPTV电视系统赋能校园电化教育系统升级
  • 为什麼建网站要先做数据库/镇江网站建设方案
  • 怎么建设一个电影网站/今日腾讯新闻最新消息
  • 做网站能赚钱吗表情包/短链接购买
  • 做淘客的网站名称/谷歌排名查询
  • html5手机论坛网站模板/西地那非片
  • 哪个网站可以学做包子/郑州网站推广公司排名