NRVO(Named Return Value Optimization)和 RVO(Return Value Optimization)

函数返回机制

函数返回值的传递分为两种情况:

  • 当返回的对象大小不超过 8 字节时,通过寄存器(eax edx)返回

  • 当返回的对象大小大于 8 字节时,通过栈返回。如果返回的是 structclass 对象,即使其大小不大于 8 字节,也是通过栈返回的。

在通过栈返回的时候,栈上会有一块空间来保存函数的返回值。当函数结束的时候,会把要返回的对象拷贝到这块区域,对于内置类型是直接拷贝,类类型的话是调用拷贝构造函数。这块区域又称为函数返回的临时对象


为了提升程序的性能,编译器会在某些情况下,会对代码进行优化。

例如:

#include <iostream>
#include <string>
#include <memory>
#include <typeinfo>

class Person {
    public:
        Person() {
            name_ = "Alice";
        }

        Person(const std::string &name) : name_(name) {
            std::cout << "create object.                    address: " << this << std::endl;
        }

        void printName() const {
            std::cout << "My name is " << name_ << std::endl;
        }

        ~Person() {
            std::cout << "destroy object.                   address: " << this << std::endl;
        }

        Person(const Person &obj) {
            std::cout << "run Person(const Person &obj)     address: " << this  << std::endl;
            name_ = obj.name_;
        }

        Person(const Person &&obj) {
            std::cout << "run Person(const Person &&obj)    address: " << this << std::endl;
            name_ = obj.name_;
        }
    public:
        std::string name_;
};

Person fun() {
    Person person("Alice");
    return person;
}

int main(int argc, char **argv) {
    Person person = fun();
    std::cout << "main person                       address: " << &person << std::endl;
    return 0;
}

编译器提供了一个编译选项 -fno-elide-constructors 来禁用返回值优化,先使用以下命令进行编译:

g++ -std=c++11 -fno-elide-constructors test.cpp -o test

程序运行结果:

create object.                    address: 0x7fffabfed020
run Person(const Person &&obj)    address: 0x7fffabfed0c0
destroy object.                   address: 0x7fffabfed020
run Person(const Person &&obj)    address: 0x7fffabfed0a0
destroy object.                   address: 0x7fffabfed0c0
main person                       address: 0x7fffabfed0a0
destroy object.                   address: 0x7fffabfed0a0

编译时使用的 C++ 标准不一样会导致运行的结果不一样,以下为分别使用 C++14、C++17 和 C++20 进行编译的结果:

C++14

create object.                      address: 0x7ffe6ec405c0
run Person(const Person &&obj)     address: 0x7ffe6ec40660
destroy object.                      address: 0x7ffe6ec405c0
run Person(const Person &&obj)     address: 0x7ffe6ec40640
destroy object.                     address: 0x7ffe6ec40660
main person                     address: 0x7ffe6ec40640
destroy object.                     address: 0x7ffe6ec40640


C++17

create object.                       address: 0x7fff22a438d0
run Person(const Person &&obj)     address: 0x7fff22a43950
destroy object.                     address: 0x7fff22a438d0
main person                     address: 0x7fff22a43950
destroy object.                     address: 0x7fff22a43950


C++20

create object.                      address: 0x7fff32bfee10
run Person(const Person &&obj)   address: 0x7fff32bfee90
destroy object.                     address: 0x7fff32bfee10
main person                     address: 0x7fff32bfee90
destroy object.                     address: 0x7fff32bfee90

可见在禁用返回值优化的情况下,调用的 Person 类成员函数的顺序为:

  • 调用构造函数,生成 fun() 函数内的 person 对象

  • 调用拷贝构造函数,生成临时对象,该临时对象此时位于栈中

  • 析构 fun() 函数内的 person 对象

  • 调用拷贝构造函数,将第二步生成的临时对象拷贝到 main() 函数中的局部对象 person 中

  • 调用析构函数,释放第二步生成的临时对象

  • 调用析构函数,释放 main() 函数中的局部对象 person

若编译时不禁用返回值优化,则运行结果如下:

使用以下命令进行编译:

g++ test.cpp -o test

编译后的程序运行结果如下:

create object.                    address: 0x7ffc20d84eb0
main person                       address: 0x7ffc20d84eb0
destroy object.                   address: 0x7ffc20d84eb0

RVO

RVO(Return Value Optimization),是一种编译器优化技术,通过该技术,编译器可以减少函数返回时生成临时对象的个数,从某种程度上可以提高程序的运行效率,对需要分配大量内存的类对象其值复制过程十分友好。

当一个未具名且未绑定到任何引用的临时变量被移动或复制到一个相同的对象时,拷贝和移动构造可以被省略。当这个临时对象在被构造的时候,他会直接被构造在将要拷贝/移动到的对象。当未命名临时对象是函数返回值时,发生的省略拷贝的行为被称为 RVO (返回值优化)。

RVO 优化针对的是返回一个未具名对象,也就是说 RVO 的功能是消除函数返回时创建的临时对象

#include <iostream>
#include <string>
#include <memory>
#include <typeinfo>

class Person {
    public:
        Person() {
            name_ = "Alice";
            std::cout << "create object.                    address: " << this << std::endl;
        }

        Person(const std::string &name) : name_(name) {
            std::cout << "create object.                    address: " << this << std::endl;
        }

        void printName() const {
            std::cout << "My name is " << name_ << std::endl;
        }

        ~Person() {
            std::cout << "destroy object.                   address: " << this << std::endl;
        }

        Person(const Person &obj) {
            std::cout << "run Person(const Person &obj)     address: " << this  << std::endl;
            name_ = obj.name_;
        }

        Person(const Person &&obj) {
            std::cout << "run Person(const Person &&obj)    address: " << this << std::endl;
            name_ = obj.name_;
        }
    public:
        std::string name_;
};

Person fun() {
    // Person person("Alice");
    return Person();
}

int main(int argc, char **argv) {
    Person person = fun();
    std::cout << "main person                       address: " << &person << std::endl;
    return 0;
}

使用 c++11 标准进行编译,并禁用返回值优化。编译命令如下:

g++ -std=c++11 -fno-elide-constructors test.cpp -o test

编译后的程序运行结果如下:

// 在 fun() 函数中,构造 Person 对象
create object.                    address: 0x7fff3173ed90
// 通过拷贝构造函数创建临时变量(fun() 函数定义的 Person 对象 --> 栈中的临时对象)
run Person(const Person &&obj)    address: 0x7fff3173ee10
// 析构 fun() 函数中构造的 Person 对象
destroy object.                   address: 0x7fff3173ed90
// 通过拷贝构造函数构造 main() 函数中的 Person 对象(栈中的临时对象 --> main() 函数中定义的 Person 对象)
run Person(const Person &&obj)    address: 0x7fff3173edf0
// 析构临时变量(栈中的临时对象)
destroy object.                   address: 0x7fff3173ee10
main person                       address: 0x7fff3173edf0
// 释放 main() 函数中定义的 Person 对象
destroy object.                   address: 0x7fff3173edf0

从上述运行结果可以看出,上述代码总共调用了 1 次构造函数2 次拷贝构造函数以及 3 次析构函数

此时我们去掉禁用返回值优化的选项,使用以下命令重新编译:

g++ -std=c++11 test.cpp -o test

编译后的程序运行结果如下:

create object.                    address: 0x7ffeaba20fd0
main person                       address: 0x7ffeaba20fd0
destroy object.                   address: 0x7ffeaba20fd0

可以看出,经过编译器优化后,总共调用了 1 次构造函数1 次拷贝构造函数以及 1 次析构函数

经过编译器优化后,减少了 2 次拷贝构造函数以及 2 次析构函数。

编译器明确知道函数会返回哪一个局部对象,那么编译器会把存储这个局部对象的地址存储返回值临时地址的地址进行复用,也就避免了从局部对象到临时对象的拷贝操作,这就是 RVO。

可以通过 -fno-elide-constructors 来禁用 RVO


NRVO

NRVO,又名具名返回值优化(Named Return Value Optimization),为RVO的一个变种,也是一种编译器对于函数返回值优化的方式。此特性从C++11开始支持,也就是说C++98、C++03都是没有将此优化特性写到标准中的,与RVO的不同之处在于函数返回的临时值是具名的。

#include <iostream>
#include <string>
#include <memory>
#include <typeinfo>

class Person {
    public:
        Person() {
            name_ = "Alice";
            std::cout << "create object.                    address: " << this << std::endl;
        }

        Person(const std::string &name) : name_(name) {
            std::cout << "create object.                    address: " << this << std::endl;
        }

        void printName() const {
            std::cout << "My name is " << name_ << std::endl;
        }

        ~Person() {
            std::cout << "destroy object.                   address: " << this << std::endl;
        }

        Person(const Person &obj) {
            std::cout << "run Person(const Person &obj)     address: " << this  << std::endl;
            name_ = obj.name_;
        }

        Person(const Person &&obj) {
            std::cout << "run Person(const Person &&obj)    address: " << this << std::endl;
            name_ = obj.name_;
        }
    public:
        std::string name_;
};

Person fun() {
    Person person("Alice");
    return person;
}

int main(int argc, char **argv) {
    Person person = fun();
    std::cout << "main person                       address: " << &person << std::endl;
    return 0;
}

使用 c++11 标准进行编译,并禁用返回值优化。编译命令如下:

g++ -std=c++11 -fno-elide-constructors test.cpp -o test

编译后的程序运行结果如下:

// 在 fun() 函数中,构造 Person 对象
create object.                    address: 0x7ffdb1ef6900
// 通过拷贝构造函数创建临时变量(fun() 函数中定义的 person --> 临时对象)
run Person(const Person &&obj)    address: 0x7ffdb1ef69a0
// 销毁 fun() 函数中定义的 person 对象
destroy object.                   address: 0x7ffdb1ef6900
// 通过拷贝构造函数创建 main() 函数中的定义的 person 对象(临时变量 --> main() 函数中定义的 person 对象
run Person(const Person &&obj)    address: 0x7ffdb1ef6980
// 销毁临时变量
destroy object.                   address: 0x7ffdb1ef69a0
main person                       address: 0x7ffdb1ef6980
// 销毁 main() 函数中定义的 person 对象
destroy object.                   address: 0x7ffdb1ef6980

从上述输出中可以看出,总共调用 1 次构造函数2 次拷贝构造函数,以及三次析构函数

换成以下命令进行编译,不禁止编译器优化:

g++ -std=c++11 test.cpp -o test

输出如下:

create object.                    address: 0x7ffcbb60ff70
main person                       address: 0x7ffcbb60ff70
destroy object.                   address: 0x7ffcbb60ff70

与 RVO 一样,也可以通过 -fno-elide-constructors 来禁用 NRVO。

参考文章
RVO和NRVO的区别是什么? - 知乎 (zhihu.com)

posted @ 2023-05-24 17:38  AibaAsagi  阅读(82)  评论(0)    收藏  举报