复制消除(copy elision)

复制消除是一种编译器优化技术,它的核心思想是避免不必要的对象复制和移动。

当:

  • 从一个函数返回一个值语义的对象;
  • 函数的参数是一个值语义的对象;

时,通常会发生几次复制或移动操作。这些操作会创建额外的临时对象,并调用构造函数、析构函数等,导致降低程序性能。

复制消除的作用就是:在某些特定情况下,编译器会直接在目标位置构造对象,而不是先创建一个临时对象再进行复制或移动。

大多数编译器都支持复制消除并默认开启,gcc可通过-fno-elide-constructors 编译选项关闭复制消除。

RVONRVO是编译器实现复制消除的优化技术,目的是消除当从一个函数返回一个对象时引入的拷贝或移动操作。

其原理是:编译器会对代码进行调整重写,改写为按引用传递参数的函数原型,从而消除拷贝构造和移动构造。

RVO

返回值优化(Return Value Optimization,RVO)应用于函数返回一个无名临时对象 (prvalue) 的情况。在这种情况下,编译器会直接在调用者的栈帧上构造这个对象,完全跳过复制或移动构造函数。

struct Foo {
   Foo() { std::cout << "Constructed" << std::endl; }
   Foo(const Foo &) { std::cout << "Copy-constructed" << std::endl; }
   Foo(Foo &&) { std::cout << "Move-constructed" << std::endl; }
   ~Foo() { std::cout << "Destructed" << std::endl; }
};

Foo f()
{
  Foo foo;
  return foo;
}
int main()
{
    Foo foo = f();
}

关闭拷贝消除编译选项,output:

Constructed
Move-constructed
Destructed
Move-constructed
Destructed
Destructed

开启拷贝消除编译选项,output:

Constructed
Destructed

NRVO

具名返回值优化(Named Return Value Optimization,NRVO)用于函数返回一个具名本地对象 (lvalue) 的情况。当函数中创建了一个局部对象,并在return语句中返回这个局部对象时,编译器可能会应用NRVO。

Foo f()
{
  return Foo();
}
int main()
{
    Foo foo = f();
}

关闭复制消除编译选项,output:

Constructed
Move-constructed
Destructed
Move-constructed
Destructed
Destructed

开启复制消除编译选项,output:

Constructed
Destructed

这个优化节省了两次(移动)构造函数的调用,第一次复制动作是将局部对象 foo 复制到函数 f() 返回值的临时对象中,第二次复制动作是将函数返回的临时对象复制到 main() 函数中的 foo 对象中。

函数值传递优化

void f(Foo f)
{
    std::cout << "Fn" << std::endl;
}

int main()
{
    f(Foo());
}

关闭复制消除编译选项,output:

Constructed
Move-constructed
Fn
Destructed
Destructed

开启复制消除编译选项,output:

Constructed
Fn
Destructed

优化失效

一些特殊场景下,是不会进行RVO/NRVO优化的。

  • 运行时根据不同分支返回不同对象。
  • 返回值是全局变量。
  • 返回值是函数的入参。
  • 返回值是一个对象的成员变量。
  • 返回值使用std::move()

有保证的复制消除

在C++17之前,RVO和NRVO都是可选的编译器优化。这意味着即使代码符合RVO/NRVO的条件,编译器也可能出于某种原因不执行优化。

为了解决这个问题,C++17 引入了有保证的复制消除(Guaranteed Copy Elision)

有保证有保证的复制消除 是一个语言规则,而不再是可选的优化。它强制编译器在某些特定情况下执行复制消除。

但它主要应用于RVO的情况,即函数返回一个无名临时对象 (prvalue)。 NRVO仍然是一个可选的优化。即使在C++17之后,编译器仍有权决定是否执行 NRVO。

struct Foo {
  Foo() { std::cout << "Constructed" << std::endl; }
  Foo(const Foo &) = delete;
  Foo(const Foo &&) = delete;
  ~Foo() { std::cout << "Destructed" << std::endl; }
};

Foo f() {
  return Foo();
}

int main() {
  Foo foo = f();
}

上述代码,在c++11中无法通过编译,但是在c++17中可以,因为不涉及移动构造或拷贝构造。

在C++17之前,如果关闭复制消除编译选项,则编译器不会进行RVO和NRVO优化。

在C++17及以后,如果关闭复制消除编译选项,编译器还是会进行RVO优化,但不会进行NRVO优化。