文章524
标签277
分类67

C++编译器优化中的RVO和NRVO

在前面的文章《深入理解C++中的move和forward》中,我们提到了为了避免对数据进行复制,在C++11中增加了move语义;

本文继续这个话题,来聊聊关于编译器优化中的RVO;

关于前一篇文章:

源代码:


C++编译器优化中的RVO和NRVO

简述

RVO 即 “Return Value Optimization”,是一种编译器优化技术,通过该技术编译器可以减少函数返回时生成临时值(对象)的个数,从某种程度上可以提高程序的运行效率,对需要分配大量内存的类对象其值复制过程十分友好;

NRVO 全称为 “Named Return Value Optimization”,该优化的大致流程与 RVO 类似;

只是单纯这么说显得比较空洞,下面来看一个具体的例子;


一个RVO优化的例子

来看下面这个例子:

rvo.cc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>

class A {
public:
A() {
std::cout << "[C] constructor fired." << std::endl;
}

A(const A &a) {
std::cout << "[C] copying constructor fired." << std::endl;
}

A(A &&a) noexcept {
std::cout << "[C] moving copying constructor fired." << std::endl;
}

~A() {
std::cout << "[C] destructor fired." << std::endl;
}
};

A getTempA() {
return A{};
}

int main(int argc, char **argv) {
auto x = getTempA();

return 0;
}

我们声明了一个类 A,并重载了它的:

  • 构造函数;
  • 复制构造函数;
  • 移动构造函数;
  • 析构函数;

在 main 函数中,我们调用 getTempA(),返回一个类A的对象;

我们首先使用下面的命令编译并执行代码:

1
2
# Use RVO
g++ rvo.cc -o rvo --std=c++11 && ./rvo

输出如下所示:

1
2
# [C] constructor fired.
# [C] destructor fired.

可以看到:这里一共只执行了一次构造函数和一次析构函数,对于临时对象的拷贝构造过程并没有被进行!

这是因为:在正常情况下,编译器一般会默认启用 RVO 优化;

接下来使用下面命令重新编译并执行上述代码:

1
2
# No RVO
g++ rvo.cc -o rvo --std=c++11 -fno-elide-constructors && ./rvo

这里我们关闭了 RVO 优化,对应使用的编译器参数是 “-fno-elide-constructors”:

1
2
# No RVO
g++ rvo.cc -o rvo --std=c++11 -fno-elide-constructors && ./rvo

不仅仅是 g++ 编译器,对于其他主流编译器(比如,clang等)都是类似的!

输出如下所示:

1
2
3
4
5
6
[C] constructor fired.
[C] moving copying constructor fired.
[C] destructor fired.
[C] moving copying constructor fired.
[C] destructor fired.
[C] destructor fired.

可以看到,这里一共执行了两次移动构造的过程,分别发生于:

  • 函数 “getTempA” 的返回值移动构造给临时值对象;
  • 以及,临时值对象移动构造给变量 “x” 这两个时刻;

实际上,RVO 以及 NRVO 两种优化技术又被统称为 “copy_elision(复制消除)” 优化;

C++11 标准中规定,在以下两种场景下要求,编译器省略类对象的复制和移动构造,即使复制/移动构造函数和析构函数拥有可观察副作用!而直接将对象构造到它们本来要复制/移动到的存储中;

只要语言确保不发生复制/移动操作,复制/移动构造函数就不必存在或可访问!

这两种场景分别是:

  • 在 “return” 语句中,当操作数为与函数返回类型为同一类类型的纯右值时;
  • 在变量的初始化中,当初始化器表达式为与变量类型为同一类类型的纯右值时:

场景一即为我们之前示例代码中的场景;

下面展示第二种场景;

实际上,我们只需要修改 main 函数中变量 x 的初始化表达式即可:

1
2
3
4
5
6
int main(int argc, char **argv) {
-- auto x = getTempA();
++ auto x = A(A(A(getTempA())));

return 0;
}

当编译器开启 RVO 优化时,程序同样只调用了一次构造函数和一次析构函数,此时输出为:

1
2
[C] constructor fired.
[C] destructor fired.

当关闭了 RVO 优化时的输出则如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
[C] constructor fired.
[C] moving copying constructor fired.
[C] destructor fired.
[C] moving copying constructor fired.
[C] moving copying constructor fired.
[C] moving copying constructor fired.
[C] moving copying constructor fired.
[C] destructor fired.
[C] destructor fired.
[C] destructor fired.
[C] destructor fired.
[C] destructor fired.

可以看到,移动构造和析构函数被疯狂的调用;

这时产生的五次拷贝构造过程分别是:

  1. 函数 “getTempA” 返回值拷贝构造给临时值对象;
  2. 临时值对象作为引用参数被类 A 的拷贝构造函数调用,生成一个 A 的临时值对象;
  3. 临时值对象作为引用参数被类 A 的拷贝构造函数调用,生成一个 A 的临时值对象;
  4. 临时值对象作为引用参数被类 A 的拷贝构造函数调用,生成一个 A 的临时值对象;
  5. 临时值对象最后拷贝构造给变量 “x”;

需要注意的是:RVO(和下面的NRVO)可能导致优化和非优化程序之间的不同行为!

毕竟某些代码被优化掉了,因此应当尽量避免在这些被优化的构造函数中增加tricky的逻辑!


再来看看NRVO优化

NRVO 与 RVO 的不同之处在于:函数返回的临时值是否是具名的;

更加官方的定义为:

当操作数是拥有自动存储期的非 volatile 对象的字段,并且非函数形参或 catch 子句形参,且其具有与函数返回类型相同的类型时,此时仍然可以避免对象复制;同时,这种复制消除的变体被称为 NRVO;

同样的,来看下面这个例子:

nrvo.cc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>

class RVO {
public:

RVO() { printf("I am in constructor\n"); }

RVO(const RVO &c_RVO) { printf("I am in copy constructor\n"); }

~RVO() { printf("I am in destructor\n"); }

int mem_var{};
};

RVO MyMethod(int i) {
RVO rvo;
rvo.mem_var = i;
return (rvo);
}

int main() {
RVO rvo;
rvo = MyMethod(5);
}

上面的代码在没有进行 NRVO 的情况下输出为:

1
2
3
4
5
6
I am in constructor
I am in constructor
I am in copy constructor
I am in destructor
I am in destructor
I am in destructor

使用 NRVO 优化后的输出将是:

1
2
3
4
I am in constructor
I am in constructor
I am in destructor
I am in destructor

下面来分析 NRVO 到底做了什么;

当我们未开启 NRVO 时,上面 MyMethod 函数会执行类似于下面的代码:

1
2
3
4
5
6
7
RVO&& MyMethod(int i) {
RVO rvo = RVO();
rvo.mem_var = i;
_rvo = RVO(&rvo);
return _rvo;
rvo.~RVO()
}

从上面的代码可以看出,有一些优化的机会;

其基本思想是消除基于栈的临时值并使用隐藏的参数;因此,这将消除基于栈的值的拷贝构造函数和析构函数;

当开启了 NRVO 后,代码如下:

1
2
3
4
5
RVO&& MyMethod(int i) {
RVO rvo = RVO();
rvo.mem_var = i;
return rvo;
}

一个更复杂的例子

下面给出了一个更复杂的例子:

nrvo_2.cc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <iostream>

class A {
public:
A() {
printf("A: I am in constructor\n");
i = 1;
}

~A() {
printf("A: I am in destructor\n");
i = 0;
}

A(const A &a) {
printf("A: I am in copy constructor\n");
i = a.i;
}

int i, x{}, w{};
};

class B {
public:
A a;

B() { printf("B: I am in constructor\n"); }

~B() { printf("B: I am in destructor\n"); }

B(const B &b) { printf("B: I am in copy constructor\n"); }
};

A MyMethod() {
B *b = new B();
A a = b->a;
delete b;
return (a);
}

int main() {
A a;
a = MyMethod();
}

无 NRVO 时输出如下:

1
2
3
4
5
6
7
8
9
10
A: I am in constructor
A: I am in constructor
B: I am in constructor
A: I am in copy constructor
B: I am in destructor
A: I am in destructor
A: I am in copy constructor
A: I am in destructor
A: I am in destructor
A: I am in destructor

当 NRVO 启动时,输出是:

1
2
3
4
5
6
7
8
A: I am in constructor
A: I am in constructor
B: I am in constructor
A: I am in copy constructor
B: I am in destructor
A: I am in destructor
A: I am in destructor
A: I am in destructor

优化限制

根据之前所述,在某些情况下即使我们启用了 NRVO 优化,优化也不会真正启动;

异常示例

在遇到异常时,隐藏的参数必须在它正在替换的临时范围内被破坏;

nrvo_with_exception.cc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <cstdio>

class RVO {
public:

RVO() { printf("I am in constructor\n"); }

RVO(const RVO &c_RVO) { printf("I am in copy constructor\n"); }

~RVO() { printf("I am in destructor\n"); }

int mem_var{};
};


RVO MyMethod(int i) {
RVO rvo;
rvo.mem_var = i;
throw "I am throwing an exception!";
return (rvo);
}

int main() {
RVO rvo;
try {
rvo = MyMethod(5);
}
catch (const char *str) {
printf("I caught the exception\n");
}

return 0;
}

如果没有NRVO,输出是:

1
2
3
4
5
I am in constructor
I am in constructor
I am in destructor
I caught the exception
I am in destructor

如果“抛出异常代码”被注释掉,输出将是:

1
2
3
4
5
6
I am in constructor
I am in constructor
I am in copy constructor
I am in destructor
I am in destructor
I am in destructor

现在,如果“抛出异常代码”被注释掉,并且开启了NRVO,输出将如下所示:

1
2
3
4
I am in constructor
I am in constructor
I am in destructor
I am in destructor

也就是说在没有NRVO的情况下,会表现出相同的行为;


不同的命名对象示例

若要使用 NRVO,要求所有退出路径必须返回同一个命名对象!

来看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <cstdio>

class RVO {
public:

RVO() { printf("I am in constructor\n"); }

RVO(const RVO &c_RVO) { printf("I am in copy constructor\n"); }

int mem_var{};
};

RVO MyMethod(int i) {
RVO rvo;
rvo.mem_var = i;
if (rvo.mem_var == 10)
return (RVO());
return (rvo);
}

int main() {
RVO rvo;
rvo = MyMethod(5);
}

无论是否启用 NRVO 结果都是相同的:

1
2
3
I am in constructor
I am in constructor
I am in copy constructor

这是因为并非所有返回都返回相同的对象;

如果将上面的示例更改为直接返回 rvo 对象,则在返回时,优化将消除复制构造函数:

1
2
3
4
5
6
7
8
RVO MyMethod(int i) {
RVO rvo;
rvo.mem_var = i;
if (rvo.mem_var == 10)
-- return (RVO());
++ return rvo;
return rvo;
}

此时输出为:

1
2
I am in constructor
I am in constructor

优化副作用

前面提到,由于我们在编译优化时,会将构造函数直接优化掉,因此:

如果在构造函数中存在一些逻辑,则会直接被优化掉!

以下面的代码为例:

nrvo_side_effects.cc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <cstdio>

int NumConsCalls = 0;
int NumCpyConsCalls = 0;

class RVO {
public:
RVO() { NumConsCalls++; }

RVO(const RVO &c_RVO) { NumCpyConsCalls++; }
};

RVO MyMethod() {
RVO rvo;
return (rvo);
}

int main() {
RVO rvo;
rvo = MyMethod();
int Division = NumConsCalls / NumCpyConsCalls;
printf("Constructor calls / Copy constructor calls = %d\n", Division);

return 0;
}

当编译未启用优化时,代码如我们所期望的,“构造函数”被调用两次,“拷贝构造函数”被调用一次;

因此输出结果为:

1
Constructor calls / Copy constructor calls = 2

但是,如果上面的代码启用 NRVO,则“拷贝构造函数”调用将被删除;

因此,此时 NumCpyConsCalls 为零,将导致异常:

1
Constructor calls / Copy constructor calls = 0

Appendix

关于前一篇文章:

源代码:

文章参考:



本文作者:Jasonkay
本文链接:https://jasonkayzk.github.io/cn/2022/05/11/C++%E7%BC%96%E8%AF%91%E5%99%A8%E4%BC%98%E5%8C%96%E4%B8%AD%E7%9A%84RVO%E5%92%8CNRVO/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可