首页 > 编程语言 >[C++] std::optional与RVO:最高效的std::optional实践与探究

[C++] std::optional与RVO:最高效的std::optional实践与探究

时间:2023-09-04 22:01:49浏览次数:41  
标签:std return temp int RVO someFn optional

返回值优化RVO

在cppreference中,是这么介绍RVO的

In a return statement, when the operand is the name of a non-volatile object with automatic storage duration, which isn't a function parameter or a catch clause parameter, and which is of the same class type (ignoring cv-qualification) as the function return type. This variant of copy elision is known as NRVO, "named return value optimization."

即在返回函数内部临时变量(非函数参数,非catch参数)时,如果该参数的的类型和函数返回值类型相同,编译器就被允许去直接构造返回值(即使copy/move构造函数具有副作用)。

std::optional

std::optional是在C++17引入的,常用于有可能构造失败的函数,作为函数的返回值。

cppreference中,std::optional的例子如下:

#include <iostream>
#include <optional>
#include <string>
 
// optional can be used as the return type of a factory that may fail
std::optional<std::string> create(bool b)
{
    if (b)
        return "Godzilla";
    return {};
}
 
// std::nullopt can be used to create any (empty) std::optional
auto create2(bool b)
{
    return b ? std::optional<std::string>{"Godzilla"} : std::nullopt;
}
 
int main()
{
    std::cout << "create(false) returned "
              << create(false).value_or("empty") << '\n';
 
    // optional-returning factory functions are usable as conditions of while and if
    if (auto str = create2(true))
        std::cout << "create2(true) returned " << *str << '\n';
}

一个尴尬的情况是这个例子并没有介绍在函数内部构造一个左值变量然后返回的情况,于是乎网上就出现了很多种return optional的写法。本文就想探讨下究竟哪一种写法才是最高效的。

实验

参数

编译器:x86-64 gcc 13.2

编译参数 -O1 -std=c++17

基于compiler explorer

准备工作

假设我们原始的函数具有以下形式

A always_success_0(int n) {
    A temp(someFn(n));
    return temp;
}

如果单纯作为可能fail的函数的一层包装,一种很自然的想法是只把函数的返回值改为std::optional,而函数体不变,即

optional<A> introduce_option_0(int n) {
    A temp(someFn(n));
    return temp;
}

很明显这会破坏NRVO的条件,但究竟相差多少呢?有没有挽回办法?

我找了网上目前常见的写法,我们可能有以下变体

optional<A> introduce_option_0(int n) {
    A temp(someFn(n));
    return temp;
}

optional<A> introduce_option_1(int n) {
    A temp(someFn(n));
    return std::move(temp);
}

optional<A> introduce_option_2(int n) {
    A temp(someFn(n));
    return {temp};
}

optional<A> introduce_option_3(int n) {
    A temp(someFn(n));
    return {std::move(temp)};
}

为了探究NRVO的条件和优化程度,对原本的函数也使用这4种变体

A always_success_0(int n) {
    A temp(someFn(n));
    return temp;
}

A always_success_1(int n) {
    A temp(someFn(n));
    return std::move(temp);
}

A always_success_2(int n) {
    A temp(someFn(n));
    return {temp};
}

A always_success_3(int n) {
    A temp(someFn(n));
    return {std::move(temp)};
}

同时让我们定义struct A

struct A{
    int ctx;
    A(int x) noexcept {
        ctx=x+1;
        printf("default construct");
        }
    A(const A&) noexcept {
        printf("copy construct");
    }
    A(A&& ano) noexcept {
        printf("move construct");
    }
    ~A() noexcept {
        printf("destruct");
    }
};

tips:

使用noexcept使编译器允许进一步优化,否则汇编会增加一段异常处理,如下图所示
无noexcept汇编代码

有noexcept汇编代码

同时为了方便定位,防止编译器进一步优化,我们将someFn写成一个具有副作用的函数

int someFn(int n) {
    int x;
    scanf("%d",&x);
    return x+n;
}

现在我们有了进行编译的所有代码:

#include <cstdio>
#include <optional>
using std::optional;

int someFn(int n) {
    int x;
    scanf("%d",&x);
    return x+n;
}

struct A{
    int ctx;
    A(int x) noexcept {
        ctx=x+1;
        printf("default construct");
        }
    A(const A&) noexcept {
        printf("copy construct");
    }
    A(A&& ano) noexcept {
        printf("move construct");
    }
    ~A() noexcept {
        printf("destruct");
    }
    A& operator=(const A&) {
        printf("copy op");
    }
    A& operator=(A&&) {
        printf("move op");
    }
};

A always_success_0(int n) {
    A temp(someFn(n));
    return temp;
}

A always_success_1(int n) {
    A temp(someFn(n));
    return std::move(temp);
}

A always_success_2(int n) {
    A temp(someFn(n));
    return {temp};
}

A always_success_3(int n) {
    A temp(someFn(n));
    return {std::move(temp)};
}

optional<A> introduce_option_0(int n) {
    A temp(someFn(n));
    return temp;
}

optional<A> introduce_option_1(int n) {
    A temp(someFn(n));
    return std::move(temp);
}

optional<A> introduce_option_2(int n) {
    A temp(someFn(n));
    return {temp};
}

optional<A> introduce_option_3(int n) {
    A temp(someFn(n));
    return {std::move(temp)};
}

编译

汇编1

我们可以看到always_success_0函数发生了RVO,只调用了一次构造函数。而always_success_1没有进行RVO,额外调用了移动构造函数和析构函数,这也是滥用std::move的一个后果。

汇编2

再看到introduce_option_0函数,它与发生移动的always_success的汇编代码相比,只多了一行设置std::optional::_Has_value布尔值的汇编。

函数 默认构造 拷贝构造 移动构造 析构 设置bool
always_success_0 1
always_success_1 1 1 1
always_success_2 1 1 1
always_success_3 1 1 1
introduce_option_0 1 1 1 1
introduce_option_1 1 1 1 1
introduce_option_2 1 1 1 1
introduce_option_3 1 1 1 1
*modify_reference *2 *1 1

*为UE库中一些形如以下的函数,

bool modify_reference(int n, A& out) {
    out = someFn(n);
    return true;
}

*算上了函数调用前的接收者的默认构造

*函数内会调用移动赋值=而不是移动构造

Best result

可以观察到,触发了RVO的汇编会精简很多,我们要想方设法去触发RVO。以下两种改良都可以触发RVO

A not_always_success_best(int n, bool &b) {
    A temp(someFn(n));
    b = true;
    return temp;
}

optional<A> optional_best(int n) {
    optional<A> temp(someFn(n));
    return temp;
}

best practice

可以看到这两种方式的函数体的汇编是一样的,不一样的只有参数传递时对栈的操作。

总结

std::optional最高效的写法是触发RVO的写法,即:

optional<A> optional_best(int n) {
    optional<A> temp(someFn(n));
    return temp;
}

标签:std,return,temp,int,RVO,someFn,optional
From: https://www.cnblogs.com/Nikola-K/p/17678203.html

相关文章

  • Swift 可选值(Optional Values)介绍
    文章转载于https://blog.csdn.net/zhangao0086/article/details/38640209Optional的定义Optional也是Objective-C没有的数据类型,是苹果引入到Swift语言中的全新类型,它的特点就和它的名字一样:可以有值,也可以没有值,当它没有值时,就是nil。此外,Swift的nil也和Objective-C有些不一样,......
  • 无涯教程-JavaScript - STDEV函数
    STDEV函数替代Excel2010中的STDEV.S函数。描述该函数根据样本估算标准偏差。标准偏差是对值与平均值(平均值)的分散程度的度量。语法STDEV(number1,[number2],...)争论Argument描述Required/OptionalNumber1Thefirstnumberargumentcorrespondingtoasampleo......
  • FastDFS开启token防盗链
     在实际的项目开发中,是不是迂到这样的问题,输入一个完的地址后,就显示出了相对应的图片。为了预防这类问题,所以使用到fdfs的token;  开启token后,在访问这个地址  一、修改fdfs的http.conf配置文件   cd/etc/fdfs/  vihttp.conf##开启token #服务器配......
  • std::for_each易忽略点
     以下代码为修改vector内部的每一个元素,使其每个元素大小变为原来的平方。std::vectorv1{1,2,4,2};std::for_each(begin(v1),end(v1),[](auto&n){returnn*n;});for(constauto&item:v1)std::cout<<item<<''; ......
  • msvc++中的预编译头文件pch.hpp和stdafx.h
    预编译头文件在VisualStudio中创建新项目时,会在项目中添加一个名为pch.h的“预编译标头文件”。(在VisualStudio2017及更高版本中,该文件名为stdafx.h)此文件的目的是加快生成过程。应在此处包含任何稳定的标头文件,例如标准库标头(如)。预编译标头仅在它或它包含的任何......
  • std模版库 队列、优先队列、双端队列
    queue为单端队列deque为双端队列priority_queue为优先队列includeincludepriority_queue<int,vector,less>//最大堆默认为对大堆也即和priority_queue等价priority_queue<int,vector,greater>//最小堆......
  • C++11 右值引用&&、移动语义std::move、完美转发std::forward
    参考:https://blog.csdn.net/HR_Reborn/article/details/130363997 #pragmaonceclassArray{public:Array():size_(0),data_(nullptr){}Array(intsize):size_(size){data_=newint[size_];}//复制构造函数(深拷贝构造)A......
  • 关于自建Rustdesk 远程桌面服务器的公网访问:无法连接中继服务器的问题解决方法
    自建服务器位于内网时,内网客户端ID/中继的地址通常写成内网IP,外网客户端一般会用公网IP进行端口映射,但这样设置出现外网客户端无法连接中继服务器,但内网客户端使用正常的现象。经过半天的排查分析,当内网和外网填写的自定义服务器地址时,rust服务器无法识别出需要使用nat包的地址,默......
  • c++ stl std::sort使用例子
    classUser{public:int32_tm_fight_power;private:int32_tm_level;};boolCenterData::compare(constUser*left,constUser*right){if(left->m_fight_power!=right->m_fight_power){returnleft->m_fight_power>ri......
  • STDC网络
    为了做到实时推理,很多实时语义分割模型选用轻量骨干网络,但是由于task-specificdesign的不足,这些从分类任务中借鉴来的轻量级骨干网络可能并不适合解决分割问题。除了选用轻量backbone,限制输入图像的大小是另一种提高推理速度的常用方法,但这很容易忽略边缘附近的细节和小物体。为......