从汇编层看64位程序运行——C++的Copy Elision(复制省略)技术的实现

在C++中,Copy Elision(复制省略)是一种编译器优化技术,它允许编译器在特定情况下省略不必要的对象复制或移动操作,以提高程序的效率。这种优化技术尤其适用于那些涉及到临时对象(如函数返回值或作为函数参数传递的对象)的场景。

Copy Elision的应用场景

返回值优化(Return Value Optimization, RVO)

当函数返回一个对象时,如果没有使用命名返回值(即没有显式地声明一个变量作为返回值,而是直接在返回语句中构造了对象),编译器可以优化掉这个对象到调用者之间的复制或移动操作。

Custom craeteCustom() {
    return Custom(1);
}

int main(int argc, char* argv[]) {
    Custom custom = craeteCustom();
    return 0;
}

比如上面的代码,理论上说,craeteCustom在其栈帧上分配了一个空间,然后返回到main函数中。由于createCustom和main函数有不同的栈帧,这个位于createCustom栈帧上的Custom对象会通过复制/移动构造函数复制/移动到main函数的栈帧中。

但是现代C++编译器使用Copy Elision(复制省略)技术不进行复制/移动构造函数的调用,而使用其他方法达成上述目的。

我们可以使用下面代码测试

#include <iostream>

class Custom {
public:
    Custom(int value) : value(value) {
        std::cout << "Constructor Custom(" << value << "). this = " << this << std::endl;
    }
    
    Custom(const Custom& custom) : value(custom.value) {
        std::cout << "Copy constructor Custom(" << custom.value << "). this = " << this << " from " << &custom << std::endl;
    }

    Custom& operator=(const Custom& custom) {
        value = custom.value;
        std::cout << "Copy assignment operator Custom(" << custom.value << "). this = " << this << " from " << &custom << std::endl;
        return *this;
    }

    Custom(Custom&& custom) : value(custom.value) {
        std::cout << "Move constructor Custom(" << custom.value << "). this = " << this << " from " << &custom << std::endl;
    }

    Custom& operator=(Custom&& custom) {
        value = custom.value;
        std::cout << "Move assignment operator Custom(" << custom.value << "). this = " << this << " from " << &custom << std::endl;
        return *this;
    }

    ~Custom() {
        std::cout << "Destructor Custom(" << value << "). this = " << this << std::endl;
    }

    friend std::ostream& operator<<(std::ostream& os, const Custom& custom) {
        os << custom.value;
        return os;
    }

public:
    int get_value() const {
        return value;
    }

    void set_value(int value) {
        this->value = value;
    }

private:
    int value;
};

Custom craeteCustom() {
    return Custom(1);
}

int main(int argc, char* argv[]) {
    Custom custom = craeteCustom();
    std::cout << "main custom = " << custom << ".Address is " << &custom << std::endl;
    return 0;
}

其输出结果如下
在这里插入图片描述
可以看到这段代码并没有进行复制/移动构造函数的调用。

命名返回值优化(Named Return Value Optimization, NRVO)

即使函数使用了命名返回值,如果编译器能够确定这个命名对象在整个函数体内没有除了返回之外的用途,它仍然可以进行优化,省略掉返回时的复制或移动操作。但值得注意的是,NRVO并非所有编译器都保证会进行,它的实现依赖于编译器的具体实现。

我们对上述案例做如下修改,在craeteCustom中先生成一个命名变量,然后将其返回。

Custom craeteCustom() {
    Custom custom = Custom(1);
    return custom;
}

针对这种操作,NRVO也会生效,并不会触发复制/移动构造函数将这个对象复制/移动到main函数栈帧中。
在这里插入图片描述

C++11及以后的移动语义

在C++11及以后的版本中,引入了移动语义和std::move函数,这为减少对象复制提供了新的工具。尽管这本身不是Copy Elision的一部分,但它与Copy Elision密切相关,因为移动操作通常比复制操作要快得多。在某些情况下,编译器可能会选择移动操作来代替复制操作,但这仍然属于Copy Elision的范畴,因为它旨在减少不必要的对象拷贝开销。

禁用Copy Elision(复制省略)

针对上面这个例子,可以针对CMakelists.txt增加-fno-elide-constructors参数。它会让编译器禁用Copy Elision(复制省略)技术。

cmake_minimum_required(VERSION 3.12)
# 项目信息
# 最后一级目录为项目名称
get_filename_component(ProjectName ${CMAKE_CURRENT_SOURCE_DIR} NAME)
project(${ProjectName})

# 添加可执行文件
add_executable(${ProjectName} main.cpp)

target_compile_options(${ProjectName} PRIVATE -fno-elide-constructors)
Custom craeteCustom() {
    Custom custom = Custom(1);
    return custom;
}

禁用后,编译器会采用移动构造函数的方式,将craeteCustom中创建的对象移动到main函数中。
在这里插入图片描述

Copy Elision(复制省略)的底层实现

高级的语言和编译器封装了很多底层实现。我们需要拨云见日,通过几个实验来回答我们对其的疑问。

这个时候我们就要抛开原来的C++代码,从汇编层看其真实实现。

由于C++编译器会使用一定的规则替换掉函数或者变量的符号名称,所以我们需要借助《C++拾趣——转换编译器生成的类型名为代码中的类型名》中的方案,将有关函数名进行转换。
在这里插入图片描述

对象构造于caller还是callee

我们分别看caller(main)和callee(craeteCustom)的反汇编
在这里插入图片描述
可以看到,Custom的构造操作和代码中表达的一致——构造于callee(craeteCustom)

所以网上有些说法认为“caller直接构造对象”是错误的。

对象析构于caller还是callee

我们还是分析caller(main)和callee(craeteCustom)的反汇编
在这里插入图片描述
可以看到析构函数是在caller(main)中被调用

对象的构造和析构是否在一个过程(caller/callee)中

通过上面的反汇编分析,我们可以看到:Copy Elision(复制省略)技术让对象的构造和析构不发生在同一个过程中:构造发生在callee(craeteCustom),析构发生在caller(main)中

对象的地址空间位于caller还是callee的栈帧

我们继续追踪caller(main)和callee(craeteCustom)的反汇编。

在craeteCustom中,+25行rax寄存器保存的是我们需要构造的Custom对象的this指针。追踪rax寄存器值的来源,最终可以追踪到rdi寄存器上。而rdi寄存器的值是main函数调用craeteCustom前设置的。
在这里插入图片描述
通过下图可以发现,rdi寄存器的值最终指向main函数的的rbp-0x1c地址。这个就说明我们在callee(craeteCustom)中构造的对象位于main函数的栈帧上
在这里插入图片描述

注意事项

程序员通常不需要显式地指示编译器进行Copy Elision,这是编译器自动进行的优化。
然而,为了充分利用这一优化,程序员在编写代码时应该考虑使用按值返回(而非按引用或指针返回)和避免不必要的对象拷贝。
值得注意的是,Copy Elision并非总是可以进行,特别是在涉及复杂表达式或库函数调用的上下文中。在这些情况下,程序员可能需要手动优化代码或使用其他技术来减少不必要的拷贝开销。
总之,Copy Elision是C++中一个重要的编译器优化技术,它有助于减少程序中不必要的对象拷贝开销,从而提高程序的性能和效率。通过理解这一技术的工作原理和应用场景,程序员可以编写出更高效、更易于维护的C++代码。

总结

  • Copy Elision(复制省略)技术构造的对象位于caller的栈帧中。
  • Copy Elision(复制省略)技术构造的对象在callee中构造。
  • Copy Elision(复制省略)技术构造的对象在calller中析构。
  • Copy Elision(复制省略)技术将预先分配好的地址通过rdi寄存器传递给callee进行对象构造。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

breaksoftware

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值