首页 > 编程语言 >C++编译器中的 Copy elision 和 RVO 优化

C++编译器中的 Copy elision 和 RVO 优化

时间:2023-12-27 21:23:19浏览次数:36  
标签:return RVO mov value 编译器 C++ 返回值

一、Copy elision简介

在 C++ 计算机编程中,复制省略(Copy elision)是指一种编译器优化技术,它消除了不必要的对象复制。
常见的俩种场景下复制省略
1、纯右值参数复制构造
2、函数返回值优化(Return value optimization RVO)

1.1 纯右值参数复制构造

#include <iostream>

int num = 0;

class X{
public:
  explicit X(int) {
    std::cout << "Call X(int)" << std::endl;
  }
  X(const X&) {
    num++;
    std::cout << "Call X(const x&)" << std::endl;
  }
};

int main() {
  X x1(42);//直接调用构造函数初始化
  X x2 = X(42); //同类型纯右值复制构造,复制省略,直接在x2位置上进行构造
  //即使在复制构造函数中存在副作用,也不会进行调用
  X x3 = x1; //左值复制构造
  std::cout << num << std::endl;//1,只在左值复制调用了一次
}

二、返回值优化(Return value optimization RVO)

2.1 RVO(Return value optimization)

RVO是一种编译器优化技术,在接收对象的位置构造返回值,避免从函数返回时创建临时对象。到了C++17标准保证了函数返回临时对象不会被复制,而不再是依赖于编译器优化[1]

RVO 例子

#include <cstdio>
#include <atomic>

struct MyType {
  char buffer[100000];
};

MyType return_unknow_value() {
	return MyType();
}

int main()
{
    auto x = return_unknow_value();
    return 0;
}

相对应的汇编代码 编译器 gcc C++17

return_unknow_value():
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     QWORD PTR [rbp-8], rdi
        mov     rax, QWORD PTR [rbp-8]
        mov     edx, 100000
        mov     esi, 0
        mov     rdi, rax	//获取返回值的地址,进行构造初始化。
        call    memset
        mov     rax, QWORD PTR [rbp-8]
        leave
        ret
main:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 100000 
        lea     rax, [rbp-100000]
        mov     rdi, rax 	//在调用函数前先分配返回值的内存空间,在保存地址值。
        call    return_unknow_value()
        mov     eax, 0
        leave
        ret

从汇编代码中可以看到,编译器遇到可以RVO优化的时候,会在调用函数前先为返回值分配好地址,后调用函数,在分配好的地址上初始化构造返回值。写成C++类似的代码如下,只会对返回值构造一次,避免了临时对象的拷贝消耗。

#include <cstdio>
#include <atomic>

struct MyType {
  char buffer[100000];
};

void return_unknow_value(void* ptr) {
  //适用replace new在预先分配内上构造返回值
  MyType *x = new(ptr) MyType{};
  //对返回值进行操作
}

int main() {
  // auto x = return_unknow_value();
  void * ptr = malloc(sizeof(MyType));
  //进入函数前先分配好内存地址,将地址作为参数传入函数中
  return_unknow_value(ptr);
  return 0;
}

2.2 RVO 相对于不同版本编译器和C++标准区别

1、C++17标准保证返回临时对象不会发生拷贝,使用C++17标准无需担心可能出现拷贝的情况[1:1]
2、C++17之前标准,返回类型对象是可移动的,存在移动构造函数(定义或默认生成),否则编译会报错,而C++17标准无论移动构造和移动赋值是否被删除都会进行RVO优化。[2]
3、C++17之前标准 gcc 可以添加-fno-elide-constructors 编译参数来禁止编译进行RVO优化。

2.3 NRVO(Name Return value optimization)

NRVO与RVO类似,但适用于返回函数内部已命名的局部变量。编译器优化这个过程,允许在调用者的栈帧上直接构造局部变量,避免了将局部变量拷贝到返回值的过程,但是NRVO并不能保证每次都会进行优化,在有一些情况不会发生,不同编译器情况也不太一样,依赖于编译器实现。

NRVO的简单例子

MyType return_name_value() {
	MyType x; //返回值具有名
	return x;
}

2.4 在以下情况不会进行NRVO优化

  • 运行时依赖(根据不同的条件分支,返回不同变量)
    例子
#include <cstdio>
#include <atomic>

struct MyType {
  char buffer[100000];
};


MyType return_name_value(bool test) {
    if (test) {
        MyType x;
        x.buffer[0] = '\0';
        return x;
    } else {
        MyType y;
        return y;
    }
}

int main()
{
    auto x = return_name_value(true);
    return 0;
}

能否对俩个分支都进行NRVO优化,依赖于编译器的实现
上面代码gcc -O3 的汇编指令

return_name_value(bool) [clone .part.0]:
        sub     rsp, 100008
        mov     edx, 100000
        mov     rsi, rsp
        call    memcpy
        add     rsp, 100008
        ret
return_name_value(bool):
        push    rbx
        mov     rbx, rdi
        test    sil, sil
        je      .L5
        mov     rax, rbx
        mov     BYTE PTR [rdi], 0
        pop     rbx
        ret
.L5:
        call    return_name_value(bool) [clone .part.0]
        mov     rax, rbx
        pop     rbx
        ret
main:
        xor     eax, eax
        ret

当test为true时,gcc会在栈上重新分配内存,然后在将内存拷贝到之前的返回值地址上。
当test为false时,发生NRVO优化

而使用clang编译 -O3参数的产生的汇编

return_name_value(bool):                 # @return_name_value(bool)
        mov     rax, rdi
        test    esi, esi
        je      .LBB0_2
        mov     byte ptr [rax], 0
.LBB0_2:                                # %return
        ret
main:                                   # @main
        xor     eax, eax
        ret

可以看到 clang编译器能够处理分支并实现NRVO优化
总结:当返回值是具名局部变量时,是否能进行NRVO优化主要依赖于具体编译器的实现

  • 返回函数参数不会发生NRVO优化[3]

这里给出来的解释,在于函数参数的控制权和生命周期在函数内部,随着函数结束而结束.具体解释看引用文章

  • 返回值是全局变量也不会发生优化

这是因为全局变量的生命周期是随程序周期的,因此即使像RVO那样,预留返回值的内存空间,返回时依旧需要对全局变量进行拷贝.

  • 返回值使用move进行转换也不会发生优化[4]
MyType return_name_value() {
    MyType y;
    return std::move(y);
}

int main()
{
    auto x = return_name_value();
    return 0;
}

gcc 对应汇编 -O3 C++17

return_name_value():
        sub     rsp, 100008
        mov     edx, 100000
        mov     rsi, rsp
        call    memcpy
        add     rsp, 100008
        ret
main:
        xor     eax, eax
        ret

可以看到即使条件满足,编译器也不会进行RVO优化,这是因为move操作将返回值转换成右值,这里语义变成了返回一个对象的引用,而RVO实施的条件是返回一个对象值.在关于RVO标准中:当RVO的前提条件允许时,要么发生复制省略,要么std::move隐式地实施于返回的局部对象上.因此对于局部对象可用于RVO优化的,不必添加move操作.

五、总结

  • 纯右值参数复制构造,不会调用复制构造函数,而是会进行复制省略优化
  • 返回值优化分为RVO(返回临时变量情况) 和 NRVO(返回具名局部变量)
  • 当函数返回临时变量时,且使用C++17标准及以上了,保证了返回临时对象不会发生拷贝(RVO)
  • 当返回的是具名的局部变量时,具体优化依赖于编译器实现
  • 当返回值是函数参数,全局变量 不会进行NRVO优化
  • 若局部对象可能适用于返回值优化,则请勿针对其实施std::move操作和std::forward.

六、引用


  1. https://stackoverflow.com/questions/12953127/what-are-copy-elision-and-return-value-optimization/12953145#12953145 ↩︎ ↩︎

  2. https://en.cppreference.com/w/cpp/language/copy_elision ↩︎

  3. https://stackoverflow.com/questions/9444485/why-is-rvo-disallowed-when-returning-a-parameter ↩︎

  4. Effective Modern C ++ 条款25 ↩︎

标签:return,RVO,mov,value,编译器,C++,返回值
From: https://www.cnblogs.com/chen-pi/p/17931460.html

相关文章

  • C++ Qt开发:TableView与TreeView组件联动
    Qt是一个跨平台C++图形界面开发库,利用Qt可以快速开发跨平台窗体应用程序,在Qt中我们可以通过拖拽的方式将不同组件放到指定的位置,实现图形化开发极大的方便了开发效率,本章将重点介绍TableView与TreeView组件联动的常用方法及灵活运用。本章我们继续实现表格的联动效果,当读者点击T......
  • 【归并排序】之C++实现
    描述归并排序是一种经典的排序算法,采用分治的思想。归并排序是一种基于分治思想的经典排序算法。它将待排序的数组不断地分成两个子数组,直到每个子数组只有一个元素。然后,对每个子数组进行归并排序,即不断地将两个有序的子数组合并成一个有序的数组。最终,所有子数组都合并成一个有......
  • C++ Qt开发:数据库与TableView多组件联动
    Qt是一个跨平台C++图形界面开发库,利用Qt可以快速开发跨平台窗体应用程序,在Qt中我们可以通过拖拽的方式将不同组件放到指定的位置,实现图形化开发极大的方便了开发效率,本章将重点介绍TableView组件与数据库联动的常用方法及灵活运用。在Qt中,通常我们不会在TableView等组件中保存数......
  • QT 中配置 64位kafka ,c++
    在MSYS2下,执行$pacman-Smingw32/mingw-w64-i686-librdkafkamingw64/mingw-w64-x86_64-librdkafka即可获得二进制库、头文件和动态链接库。文件路径实例,D:\msys64\mingw64下找文件即可:D:\msys64\mingw64\lib\librdkafka++.dll.a 在工程文件中创建文件夹thirdparty/librdkaf......
  • 有什么好用的C/C++源代码混淆工具?
    开始使用ipaguard前言iOS加固保护是直接针对iosipa二进制文件的保护技术,可以对iOSAPP中的可执行文件进行深度混淆、加密。使用任何工具都无法逆向、破解还原源文件。对APP进行完整性保护,防止应用程序中的代码及资源文件被恶意篡改。IpaGuard通过修改ipa文件中的macho文件......
  • C++ Qt开发:QSqlDatabase数据库组件
    Qt是一个跨平台C++图形界面开发库,利用Qt可以快速开发跨平台窗体应用程序,在Qt中我们可以通过拖拽的方式将不同组件放到指定的位置,实现图形化开发极大的方便了开发效率,本章将重点介绍QSqlDatabase数据库模块的常用方法及灵活运用。QtSQL模块是Qt框架的一部分,它提供了一组类和函数......
  • C/C++中的宏相关操作
    C++中的宏具有一些高级用法,以下是其中的一些:可变参数宏:使用...;表示可变参数,在宏里对可变参数进行操作。比如使用 __VA_ARGS__ 来代表可变参数。字符串拼接:使用# 操作符,可以将参数转换为字符串。例如,#defineSTRINGIFY(x)#x 可以将 x 转换为字符串。标记连接:使用......
  • C++U4-第10课-前缀和与差分
    学习目标 前缀和解决的问题 前缀和概念 前缀和构建方式  前缀和主要解决区间求和问题练习题1:[前缀和]【算法分析】前缀和数组s的含义是s[i]表示a[1]~a[i]的和,那么∑i=li=r​a[i]=s[r]−s[l−1]。【参考代码】#include<iostream>usin......
  • Qt/C++音视频开发61-多屏渲染/一个解码渲染到多个窗口/画面实时同步
    一、前言多屏渲染就是一个解码线程对应多个渲染界面,通过addrender这种方式添加多个绘制窗体,我们经常可以在展会或者卖电视机的地方可以看到很多电视播放的同一个画面,原理应该类似,一个地方负责打开解码播放,将画面同步传输到多个显示的地方,完全保证了画面的一致性。这样相当于复用......
  • C++基础 -12- 类的析构函数
    ———————标准输入输出——————— ......