## 返回值優化RVO 在cppreference中,是這麼介紹RVO的 `In a return statement, when the operand is the name of a non-volatile object with automatic storage duration, wh ...
返回值優化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使編譯器允許進一步優化,否則彙編會增加一段異常處理,如下圖所示
同時為了方便定位,防止編譯器進一步優化,我們將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)};
}
編譯
我們可以看到always_success_0
函數發生了RVO,只調用了一次構造函數。而always_success_1
沒有進行RVO,額外調用了移動構造函數和析構函數,這也是濫用std::move
的一個後果。
再看到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;
}
可以看到這兩種方式的函數體的彙編是一樣的,不一樣的只有參數傳遞時對棧的操作。
總結
std::optional
最高效的寫法是觸發RVO的寫法,即:
optional<A> optional_best(int n) {
optional<A> temp(someFn(n));
return temp;
}