返回值优化汇编分析


返回值优化是一个很经典的问题,很多面试官也会针对返回值优化和拷贝复制和移动复制问一些问题。

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

他的作用是可以优化栈上的临时对象,并且也可以减少复制的开销。

禁止返回值优化

分析

如果我们禁止了返回值优化,那么从函数中返回对象,一种实现办法是在函数调用语句前在stack frame上声明一个隐藏对象,把该对象的地址隐蔽传入被调用函数,函数的返回对象直接构造或者复制构造到该地址上。

struct BigObject {};

BigObject foo() {
  BigObject ret;
  // generate ret
  return ret;
}

int main() {
  BigObject o = foo();
}

可能产生的代码如下:

struct BigObject {};

BigObject * foo(BigObject * _hiddenAddress) {
  BigObject ret = {};
  // copy result into hidden object
  *_hiddenAddress = ret;
  return _hiddenAddress;
}

int main() {
  BigObject _hidden; // create hidden object
  BigObject o = *foo(&_hidden); // copy the result into d
}

这引起了BigObject对象被复制两次,也就是上图中左侧图片描述的过程。

优化之后可能会产生如下的代码:

struct BigObject {};

void f(BigObject& ret_value) {
  BigObject localObj;
  return ret_value.BigObject::BigObject(std::move(localObj));//显式构造
}

int main() {
  BigObject o; ///这里没有使用默认构造,定义而不构造
  f(&o);
}

返回的类对象直接被构造在将要拷贝/移动到的对象栈空间上,只会产生一次构造/析构,优化掉了栈上的临时对象。

下面我就使用这个代码来分别看下这个代码在11和17下的汇编,来分析具体的实现。

#include <bits/stdc++.h>
using namespace std;

static int counter; // counter to identify instances

struct Data {
    int i{ 0 };
    int id;

    Data() : id{ ++counter } {
        std::cout << "ctor " << id << "\n";
    }

    Data(const Data& s) : i{ s.i }, id{ ++counter } {
        std::cout << "copy ctor " << id << "\n";
    }

    Data& operator=(const Data& data) {
        i = data.i;
        std::cout << "copy assign " << data.id << " to " << id << "\n";
        return *this;
    }

    ~Data() {
        std::cout << "dtor " << id << "\n";
    }
};
Data GetData() {
    return Data{};
}

int main() {
    Data d = GetData();  
    return 0;
}

C++11-14

在C++11到14的标准中,并没有明确的规定返回值优化必须要做,但是大部分的编译器都是执行这个优化。

使用-fno-elide-constructors​这个标志就可以了。

.LC0:
        .string "ctor "
.LC1:
        .string "\n"
Data::Data() [base object constructor]:
        push    rbp
        mov     rbp, rsp
        push    rbx
        sub     rsp, 24
        mov     QWORD PTR [rbp-24], rdi
        mov     rax, QWORD PTR [rbp-24]
        mov     DWORD PTR [rax], 0
        mov     eax, DWORD PTR counter[rip]
        add     eax, 1
        mov     DWORD PTR counter[rip], eax
        mov     edx, DWORD PTR counter[rip]
        mov     rax, QWORD PTR [rbp-24]
        mov     DWORD PTR [rax+4], edx
        mov     rax, QWORD PTR [rbp-24]
        mov     ebx, DWORD PTR [rax+4]
        mov     esi, OFFSET FLAT:.LC0
        mov     edi, OFFSET FLAT:std::cout
        call    std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
        mov     esi, ebx
        mov     rdi, rax
        call    std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
        mov     esi, OFFSET FLAT:.LC1
        mov     rdi, rax
        call    std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
        nop
        add     rsp, 24
        pop     rbx
        pop     rbp
        ret
.LC2:
        .string "copy ctor "
Data::Data(Data const&) [base object constructor]:
        push    rbp
        mov     rbp, rsp
        push    rbx
        sub     rsp, 24
        mov     QWORD PTR [rbp-24], rdi
        mov     QWORD PTR [rbp-32], rsi
        mov     rax, QWORD PTR [rbp-32]
        mov     edx, DWORD PTR [rax]
        mov     rax, QWORD PTR [rbp-24]
        mov     DWORD PTR [rax], edx
        mov     eax, DWORD PTR counter[rip]
        add     eax, 1
        mov     DWORD PTR counter[rip], eax
        mov     edx, DWORD PTR counter[rip]
        mov     rax, QWORD PTR [rbp-24]
        mov     DWORD PTR [rax+4], edx
        mov     rax, QWORD PTR [rbp-24]
        mov     ebx, DWORD PTR [rax+4]
        mov     esi, OFFSET FLAT:.LC2
        mov     edi, OFFSET FLAT:std::cout
        call    std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
        mov     esi, ebx
        mov     rdi, rax
        call    std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
        mov     esi, OFFSET FLAT:.LC1
        mov     rdi, rax
        call    std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
        nop
        add     rsp, 24
        pop     rbx
        pop     rbp
        ret
.LC3:
        .string "dtor "
Data::~Data() [base object destructor]:
        push    rbp
        mov     rbp, rsp
        push    rbx
        sub     rsp, 24
        mov     QWORD PTR [rbp-24], rdi
        mov     rax, QWORD PTR [rbp-24]
        mov     ebx, DWORD PTR [rax+4]
        mov     esi, OFFSET FLAT:.LC3
        mov     edi, OFFSET FLAT:std::cout
        call    std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
        mov     esi, ebx
        mov     rdi, rax
        call    std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
        mov     esi, OFFSET FLAT:.LC1
        mov     rdi, rax
        call    std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
        nop
        add     rsp, 24
        pop     rbx
        pop     rbp
        ret
GetData():
        push    rbp
        mov     rbp, rsp
        push    rbx
        sub     rsp, 40
        mov     QWORD PTR [rbp-40], rdi
        lea     rax, [rbp-24]
        mov     rdi, rax
        call    Data::Data() [complete object constructor]
        lea     rdx, [rbp-24]
        mov     rax, QWORD PTR [rbp-40]
        mov     rsi, rdx
        mov     rdi, rax
        call    Data::Data(Data const&) [complete object constructor]
        lea     rax, [rbp-24]
        mov     rdi, rax
        call    Data::~Data() [complete object destructor]
        jmp     .L8
        mov     rbx, rax
        lea     rax, [rbp-24]
        mov     rdi, rax
        call    Data::~Data() [complete object destructor]
        mov     rax, rbx
        mov     rdi, rax
        call    _Unwind_Resume
.L8:
        mov     rax, QWORD PTR [rbp-40]
        add     rsp, 40
        pop     rbx
        pop     rbp
        ret
main:
        push    rbp
        mov     rbp, rsp
        push    rbx
        sub     rsp, 24
        lea     rax, [rbp-24]
        mov     rdi, rax
        call    GetData()
        lea     rdx, [rbp-24]
        lea     rax, [rbp-32]
        mov     rsi, rdx
        mov     rdi, rax
        call    Data::Data(Data const&) [complete object constructor]
        lea     rax, [rbp-24]
        mov     rdi, rax
        call    Data::~Data() [complete object destructor]
        mov     ebx, 0
        lea     rax, [rbp-32]
        mov     rdi, rax
        call    Data::~Data() [complete object destructor]
        mov     eax, ebx
        jmp     .L13
        mov     rbx, rax
        lea     rax, [rbp-24]
        mov     rdi, rax
        call    Data::~Data() [complete object destructor]
        mov     rax, rbx
        mov     rdi, rax
        call    _Unwind_Resume
.L13:
        add     rsp, 24
        pop     rbx
        pop     rbp
        ret
__static_initialization_and_destruction_0(int, int):
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     DWORD PTR [rbp-4], edi
        mov     DWORD PTR [rbp-8], esi
        cmp     DWORD PTR [rbp-4], 1
        jne     .L16
        cmp     DWORD PTR [rbp-8], 65535
        jne     .L16
        mov     edi, OFFSET FLAT:std::__ioinit
        call    std::ios_base::Init::Init() [complete object constructor]
        mov     edx, OFFSET FLAT:__dso_handle
        mov     esi, OFFSET FLAT:std::__ioinit
        mov     edi, OFFSET FLAT:std::ios_base::Init::~Init() [complete object destructor]
        call    __cxa_atexit
.L16:
        nop
        leave
        ret
_GLOBAL__sub_I_GetData():
        push    rbp
        mov     rbp, rsp
        mov     esi, 65535
        mov     edi, 1
        call    __static_initialization_and_destruction_0(int, int)
        pop     rbp
        ret

结果

ctor 1
copy ctor 2
dtor 1
copy ctor 3
dtor 2
dtor 3

我们来分析一下,main函数在他的栈帧上分配了一块地址,把他赋值给rdi,虽然GetData​没有参数,但是还是要传递进去。

然后在函数的栈帧中我们在申请一块内存,用于构建新的对象。然后

lea     rdx, [rbp-24];这个是GetData栈帧上创建的对象,地址
mov     rax, QWORD PTR [rbp-40];这个是main栈帧上的那么
mov     rsi, rdx;第二个参数,拷贝构造函数
mov     rdi, rax;第一个参数,this
call    Data::Data(Data const&) [complete object constructor];这个就非常明确了,传递进去,进行拷贝赋值

然后就是调用这个析构函数。然后ret返回main的栈帧。

main的栈帧我们再次进行拷贝初始化,最后就是析构了。

C++17

现在我们使用C++17,在C++17中,确定了标准,也就是返回值必须要优化,即使使用参数也没有用。

.LC0:
        .string "ctor "
.LC1:
        .string "\n"
Data::Data() [base object constructor]:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     QWORD PTR [rbp-8], rdi
        mov     rax, QWORD PTR [rbp-8]
        mov     DWORD PTR [rax], 0
        mov     eax, DWORD PTR counter[rip]
        add     eax, 1
        mov     DWORD PTR counter[rip], eax
        mov     edx, DWORD PTR counter[rip]
        mov     rax, QWORD PTR [rbp-8]
        mov     DWORD PTR [rax+4], edx
        mov     esi, OFFSET FLAT:.LC0
        mov     edi, OFFSET FLAT:std::cout
        call    std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
        mov     rdx, rax
        mov     rax, QWORD PTR [rbp-8]
        mov     eax, DWORD PTR [rax+4]
        mov     esi, eax
        mov     rdi, rdx
        call    std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
        mov     esi, OFFSET FLAT:.LC1
        mov     rdi, rax
        call    std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
        nop
        leave
        ret
.LC2:
        .string "copy ctor "
Data::Data(Data const&) [base object constructor]:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     QWORD PTR [rbp-8], rdi
        mov     QWORD PTR [rbp-16], rsi
        mov     rax, QWORD PTR [rbp-16]
        mov     edx, DWORD PTR [rax]
        mov     rax, QWORD PTR [rbp-8]
        mov     DWORD PTR [rax], edx
        mov     eax, DWORD PTR counter[rip]
        add     eax, 1
        mov     DWORD PTR counter[rip], eax
        mov     edx, DWORD PTR counter[rip]
        mov     rax, QWORD PTR [rbp-8]
        mov     DWORD PTR [rax+4], edx
        mov     esi, OFFSET FLAT:.LC2
        mov     edi, OFFSET FLAT:std::cout
        call    std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
        mov     rdx, rax
        mov     rax, QWORD PTR [rbp-8]
        mov     eax, DWORD PTR [rax+4]
        mov     esi, eax
        mov     rdi, rdx
        call    std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
        mov     esi, OFFSET FLAT:.LC1
        mov     rdi, rax
        call    std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
        nop
        leave
        ret
.LC3:
        .string "dtor "
Data::~Data() [base object destructor]:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     QWORD PTR [rbp-8], rdi
        mov     esi, OFFSET FLAT:.LC3
        mov     edi, OFFSET FLAT:std::cout
        call    std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
        mov     rdx, rax
        mov     rax, QWORD PTR [rbp-8]
        mov     eax, DWORD PTR [rax+4]
        mov     esi, eax
        mov     rdi, rdx
        call    std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
        mov     esi, OFFSET FLAT:.LC1
        mov     rdi, rax
        call    std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
        nop
        leave
        ret
GetData():
        push    rbp
        mov     rbp, rsp
        push    rbx
        sub     rsp, 40
        mov     QWORD PTR [rbp-40], rdi
        lea     rax, [rbp-24]
        mov     rdi, rax
        call    Data::Data() [complete object constructor]
        lea     rdx, [rbp-24]
        mov     rax, QWORD PTR [rbp-40]
        mov     rsi, rdx
        mov     rdi, rax
        call    Data::Data(Data const&) [complete object constructor]
        nop
        lea     rax, [rbp-24]
        mov     rdi, rax
        call    Data::~Data() [complete object destructor]
        jmp     .L8
        mov     rbx, rax
        lea     rax, [rbp-24]
        mov     rdi, rax
        call    Data::~Data() [complete object destructor]
        mov     rax, rbx
        mov     rdi, rax
        call    _Unwind_Resume
.L8:
        mov     rax, QWORD PTR [rbp-40]
        mov     rbx, QWORD PTR [rbp-8]
        leave
        ret
main:
        push    rbp
        mov     rbp, rsp
        push    rbx
        sub     rsp, 24
        lea     rax, [rbp-24]
        mov     rdi, rax
        call    GetData()
        lea     rdx, [rbp-24]
        lea     rax, [rbp-32]
        mov     rsi, rdx
        mov     rdi, rax
        call    Data::Data(Data const&) [complete object constructor]
        lea     rax, [rbp-24]
        mov     rdi, rax
        call    Data::~Data() [complete object destructor]
        mov     ebx, 0
        lea     rax, [rbp-32]
        mov     rdi, rax
        call    Data::~Data() [complete object destructor]
        mov     eax, ebx
        jmp     .L13
        mov     rbx, rax
        lea     rax, [rbp-24]
        mov     rdi, rax
        call    Data::~Data() [complete object destructor]
        mov     rax, rbx
        mov     rdi, rax
        call    _Unwind_Resume
.L13:
        mov     rbx, QWORD PTR [rbp-8]
        leave
        ret

结果

ctor 1
dtor 1

可以很清楚的看到,只进行了一次初始化。

其余特殊的优化

所有分支返回同一个具名对象

若分支返回全是同一具名对象, 发生返回值优化. 运行代码

#include <iostream>
#include <cstddef>
#include <cstring>
#include <utility>

static int counter; // counter to identify instances of S

struct Data {
    int i{ 0 };
    int id;

    Data() : id{ ++counter } {
        std::cout << "ctor " << id << "\n";
    }

    Data(const Data& s) : i{ s.i }, id{ ++counter } {
        std::cout << "copy ctor " << id << "\n";
    }

    Data& operator=(const Data& data) {
        i = data.i;
        std::cout << "copy assign " << data.id << " to " << id << "\n";
        return *this;
    }

    ~Data() {
        std::cout << "dtor " << id << "\n";
    }
};

Data GetData(int param) {
    Data d;
  
    if (param % 2 == 0) {
        d.i = 1;
        return d;
    }
    else if (param % 2 == 1) {
        d.i = 2;
        return d;
    }

    return d;
}

int main() {
    Data d = GetData(0);  
    return 0;
}

这个返回值全部都是d,所以可以使用优化,所以可以进行优化。

所有分支返回非同一对象

若分支返回不全是同一具名对象, 则无返回值优化. 因为返回的对象在运行时确定, 编译器无法在编译期决定.

运行代码

Data GetData(int param) {
    Data d;                     // ctor 1
  
    if (param % 2 == 0) {
        d.i = 1;
        return d;
    }
    else if (param % 2 == 1) {
        Data d2;
        d2.i = 2;
        return d2;
    }

    return d;
}                               // copy ctor 2, dtor 1

int main() {
    Data d = GetData(0);  
    return 0;
}                               // dtor 2

函数返回结果用于赋值

如果调用函数时, 造成的是拷贝赋值, 而不是拷贝构造, 即使是不具名的情况, 也不会发生返回值优化 (注: 换个思路理解, 编译器不清楚赋值左侧的值从创建到赋值之间, 将处于何种状态, 或者进行何种操作, 所以不会对这种形式做返回值优化. 为避免这种情况的拷贝赋值, 可以通过移动赋值来消除).

运行代码

Data GetData(int param) {
    //!!! sub case 1
    // Data d{};            // ctor 2
    // return d;

    //!!! sub case 2
    // return Data{};       // ctor 2

    //!!! sub case 3
    Data d{};               // ctor 2
    if (param % 2 == 0) {
        d.i = 1;
        return d;
    } else {
        d.i = 2;
        return d;
    }
}

int main() {
    Data d;                 // ctor 1
    d = GetData(0);         // copy assign 2 to 1
    return 0;
}                           // dtor 2, dtor1

这个也可以理解,毕竟之前都是拷贝构造函数,因为这个对象还没有初始化,我们可以把栈上的地址传递进去。

但是如果说这个对象已经初始化了,我们怎么进行初始化,所以就没有办法来原来的地址上进行初始化了,就只能重新申请地址,把东西放到这里,然后再进行拷贝。

返回一个对象的成员

struct DataWrap {
    Data d;
};

Data GetData() {
    return DataWrap{}.d;        // ctor 1
}                               // copy ctor 2, dtor 1

int main() {
    Data d = GetData();  
    return 0;
}                               // dtor 2

这种情况下,他其实需要生成一个匿名变量,所以这个变量必须生成,所以也就无法初始化。

返回值是全局的变量或是其他变量

Data sd{};                  // ctor 1

Data GetData() {
    return sd;              
}                           // copy ctor 2, dtor 1

int main() {
    Data d = GetData();  
    return 0;
}             

对于这种嘛,也可以理解,毕竟这个对象已经生成了,只能进行拷贝赋值了。

其实上面的这些例子,当我们了解了他的原理就可以很容器的想到。我们之所以能够进行返回值优化,就是因为我们在上一个栈帧中找了一块内存,作为参数传递过去。这样才可以返回值优化,如果不能根据这样,就不能进行优化,必须需要拷贝。

还有就是cpp标准规定,return 一个表达式,如果这个表达式是一个自动生命周期变量(栈上变量),并且不是volite,那么他就是可移动的,优先使用移动构造函数。


文章作者: 张兵帅
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 张兵帅 !
  目录