个人生活分享

c++ RVO返回值优化

RVO

gcc、clang默认开启,MSVC默认关闭。可以通过编译选项-fno-elide-constructors​关闭优化。

标准布局类型

关闭返回值优化

class Object {
public:
    Object() {
        printf("Default constructor\n");
        a = b = c = d = 0;
    }
    int a;
    int b;
    int c;
    int d;
};

Object foo() {
    Object p;
    p.a = 1;
    p.b = 2;
    p.c = 3;
    p.d = 4;
    return p;
}

int main() {
    Object obj = foo();
    return 0;
}
foo():
        push    rbp
        mov     rbp, rsp
        sub     rsp, 32
        lea     rax, [rbp-32]
        mov     rdi, rax
        call    Object::Object() [complete object constructor]
        mov     DWORD PTR [rbp-32], 1
        mov     DWORD PTR [rbp-28], 2
        mov     DWORD PTR [rbp-24], 3
        mov     DWORD PTR [rbp-20], 4
        lea     rdx, [rbp-32]
        lea     rax, [rbp-16]
        mov     rsi, rdx
        mov     rdi, rax
        call    Object::Object(Object&&) [complete object constructor]
        mov     rax, QWORD PTR [rbp-16]
        mov     rdx, QWORD PTR [rbp-8]
        leave
        ret
main:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        call    foo()
        mov     QWORD PTR [rbp-16], rax
        mov     QWORD PTR [rbp-8], rdx
        mov     eax, 0
        leave
        ret

当POD类不提供拷贝构造函数,且关闭RVO返回值优化时,由编译器自动生成拷贝构造函数。

==此时19行 return p 时,发生了拷贝构造==。

开启返回值优化

1. 无定义拷贝构造函数

class Object {
public:
    Object() {
        printf("Default constructor\n");
        a = b = c = d = 0;
    }
    int a;
    int b;
    int c;
    int d;
};

Object foo() {
    Object p;
    p.a = 1;
    p.b = 2;
    p.c = 3;
    p.d = 4;
    return p;
}

int main() {
    Object obj = foo();
    return 0;
}
foo():
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        lea     rax, [rbp-16]
        mov     rdi, rax
        call    Object::Object() [complete object constructor]
        mov     DWORD PTR [rbp-16], 1
        mov     DWORD PTR [rbp-12], 2
        mov     DWORD PTR [rbp-8], 3
        mov     DWORD PTR [rbp-4], 4
        mov     rax, QWORD PTR [rbp-16]
        mov     rdx, QWORD PTR [rbp-8]
        leave
        ret
main:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        call    foo()
        mov     QWORD PTR [rbp-16], rax
        mov     QWORD PTR [rbp-8], rdx
        mov     eax, 0
        leave
        ret

汇编第6,7行构造了Object类的对象,8-11行对成员进行赋值。

==通过将对象的值拷贝到rax和rdx寄存器中作为返回值返回==

2. 定义了拷贝构造

#include <cstdio>
#include <string.h>

class Object {
public:
    Object() {
        printf("Default constructor\n");
        a = b = c = d = 0;
    }
    Object(const Object& rhs) {
        printf("Copy constructor\n");
        memcpy(this, &rhs, sizeof(Object));
    }
    int a;
    int b;
    int c;
    int d;
};

Object foo() {
    Object p;
    p.a = 1;
    p.b = 2;
    p.c = 3;
    p.d = 4;
    return p;
}

int main() {
    Object obj = foo();
    return 0;
}
foo():
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     QWORD PTR [rbp-8], rdi
        mov     rax, QWORD PTR [rbp-8]
        mov     rdi, rax
        call    Object::Object() [complete object constructor]
        mov     rax, QWORD PTR [rbp-8]
        mov     DWORD PTR [rax], 1
        mov     rax, QWORD PTR [rbp-8]
        mov     DWORD PTR [rax+4], 2
        mov     rax, QWORD PTR [rbp-8]
        mov     DWORD PTR [rax+8], 3
        mov     rax, QWORD PTR [rbp-8]
        mov     DWORD PTR [rax+12], 4
        nop
        mov     rax, QWORD PTR [rbp-8]
        leave
        ret
main:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        lea     rax, [rbp-16]
        mov     rdi, rax
        call    foo()
        mov     eax, 0
        leave
        ret

可以看到汇编代码中没有生成拷贝构造函数的代码,被编译器所优化。

但是此时汇编代码与情况1有所不同

  • sub rsp, 16 -> lea rax, [rbp-16]​预留Object​对象所需的16字节空间,将栈顶的内容加载到寄存器rax​中

  • 汇编第26行mov rdi, rax​将一个对象的地址加载到rdi​中。

  • 第27行并未调用构造函数,而是将对象的地址传入foo​函数中,在foo​函数中调用构造函数

  • mov rax, QWORD PTR [rbp-8]​此时rax值为main作用域下为对象预留空间的底部地址

  • 10-16行对对象的成员进行赋值

以上流程类似于下面所示代码

Object obj = foo();
// 将被改成:
Object obj;	// 这里不需要调用默认构造函数
foo(obj);

// 相应地foo函数将被改写定义:
void foo(Object& obj) {
    obj.Object::Object();	// 调用Object的默认构造函数
    obj.a = 1;
    obj.b = 2;
    obj.c = 3;
    obj.d = 4;
    return;
}

若用户自定义了拷贝构造,说明是要进行拷贝大块的内存空间等之类的操作,不仅仅是普通的数据成员的拷贝。如果拷贝构造函数中只是拷贝数据成员,可以不必定义拷贝构造函数,编译器会采用更高效的逐成员拷贝的方法,编译器内部就可以帮程序员做好了,所以有拷贝构造函数的存在就说明有需要低效的拷贝动作,那么就要想办法消除掉拷贝的操作,那么启用NRV优化就是一项提高效率的做法了。