共计 4321 个字符,预计需要花费 11 分钟才能阅读完成。
前言
思考存在这样一个类如 HeavyObject,其拷贝赋值操作比拟耗时,通常你在应用函数返回这个类的一个对象时会习惯应用哪一种形式?或者会依据具体场景抉择某一种形式?
// style 1
HeavyObject func(Args param);
// style 2
bool func(HeavyObject* ptr, Args param);
下面的两种形式都能过到同样的目标,但直观上的应用体验的差异也是非常明显的:
style 1 只须要一行代码,而 style 2 须要两行代码
// style 1
HeavyObject obj = func(params);
// style 2
HeavyObject obj;
func(&obj, params);
然而,能达到同样的目标,耗费的老本却未必是一样的,这取决于多个因素,比方编译器反对的个性、C++ 语言规范的标准强制性、多团队多环境开发等等。
看起来 style 2 尽管应用时须要写两行代码,但函数外部的老本却是确定的,只会取决于你以后的编译器,内部即便采纳不同的编译器进行函数调用,也并不会有多余的工夫开销和稳定性问题。比方 func 外部应用 clang+libc++ 编译,内部调用的编译环境为 gcc+gnustl 或者 vc++,除了函数调用开销,不必放心其它性能开销以及因为编译环境不同会解体问题。
因而这里我次要分析一下 style 1 背地开发者须要关注的点。
RVO
RVO 是 Return Value Optimization 的缩写,即返回值优化,NRVO 就是具名的返回值优化,为 RVO 的一个变种,此个性从 C ++11 开始反对,也就是说 C ++98、C++03 都是没有将此优化个性写到规范中的,不过大量编译器在开发过程中也会反对 RVO 优化(如 IBM Compiler?),比方微软是从 Visual Studio 2010 才开始反对的。
依然以上述的 HeavyObject 类为例,为了更清晰的理解编译器的行为,这里实现了结构 / 析构及拷贝结构、赋值操作、右值构造函数,如下
class HeavyObject
{
public:
HeavyObject() { cout << "Constructorn";}
~HeavyObject() { cout << "Destructorn";}
HeavyObject(HeavyObject const&) {cout << "Copy Constructorn";}
HeavyObject& operator=(HeavyObject const&) {cout << "Assignment Operatorn"; return *this;}
HeavyObject(HeavyObject&&) {cout << "Move Constructorn";}
private:
// many members omitted...
};
编译环境:
AppleClang 10.0.1.10010046
- 第一种应用形式
==========
HeavyObject func()
{return HeavyObject();
}
// call
HeavyObject o = func();
依照以往对 C ++ 的了解,HeavyObject 类的结构析构程序应该为
Constructor
Copy Constructor
Destructor
Destructor
然而理论运行后的输入后果却为
Constructor
Destructor
理论运行中少了一次拷贝结构和析构的开销,编译器帮忙咱们作了优化。
于是我反汇编了一下:
0000000100000f60 <__Z4funcv>:
100000f60: 55 push %rbp
100000f61: 48 89 e5 mov %rsp,%rbp
100000f64: 48 83 ec 10 sub $0x10,%rsp
100000f68: 48 89 f8 mov %rdi,%rax
100000f6b: 48 89 45 f8 mov %rax,-0x8(%rbp)
100000f6f: e8 0c 00 00 00 callq 100000f80 <__ZN11HeavyObjectC1Ev>
100000f74: 48 8b 45 f8 mov -0x8(%rbp),%rax
100000f78: 48 83 c4 10 add $0x10,%rsp
100000f7c: 5d pop %rbp
100000f7d: c3 retq
100000f7e: 66 90 xchg %ax,%ax
上述汇编代码中的__Z4funcv 即 func()函数,__ZN11HeavyObjectC1Ev 即 HeavyObject::HeavyObject()。
不同编译器的 C ++ 润饰规定略有不同。
实际上这里就是先创立内部的对象,再将内部对象的地址作为参数传给函数 func,相似 style 2 形式。
- 第二种应用形式
==========
HeavyObject func()
{
HeavyObject o;
return o;
}
// call
HeavyObject o = func();
运行上述调用代码的后果为
Constructor
Destructor
与第一种应用形式的后果雷同,这里编译器理论做了 NRVO,来看一下反汇编
0000000100000f40 <__Z4funcv>: // func()
100000f40: 55 push %rbp
100000f41: 48 89 e5 mov %rsp,%rbp
100000f44: 48 83 ec 20 sub $0x20,%rsp
100000f48: 48 89 f8 mov %rdi,%rax
100000f4b: c6 45 ff 00 movb $0x0,-0x1(%rbp)
100000f4f: 48 89 7d f0 mov %rdi,-0x10(%rbp)
100000f53: 48 89 45 e8 mov %rax,-0x18(%rbp)
100000f57: e8 24 00 00 00 callq 100000f80 <__ZN11HeavyObjectC1Ev> // HeavyObject::HeavyObject()
100000f5c: c6 45 ff 01 movb $0x1,-0x1(%rbp)
100000f60: f6 45 ff 01 testb $0x1,-0x1(%rbp)
100000f64: 0f 85 09 00 00 00 jne 100000f73 <__Z4funcv+0x33>
100000f6a: 48 8b 7d f0 mov -0x10(%rbp),%rdi
100000f6e: e8 2d 00 00 00 callq 100000fa0 <__ZN11HeavyObjectD1Ev> // HeavyObject::~HeavyObject()
100000f73: 48 8b 45 e8 mov -0x18(%rbp),%rax
100000f77: 48 83 c4 20 add $0x20,%rsp
100000f7b: 5d pop %rbp
100000f7c: c3 retq
100000f7d: 0f 1f 00 nopl (%rax)
从下面的汇编代码能够看到返回一个具名的本地对象时,编译器优化操作如第一种应用形式一样间接在内部对象的指针上执行构造函数,只是如果结构失败时还会再调用析构函数。
以上两种应用形式编译器所做的优化十分相近,两种形式的共同点都是返回本地的一个对象,那么当本地存在多个对象且须要依据条件抉择返回某个对象时后果会是如何呢?
- 第三种应用形式
==========
HeavyObject dummy(int index)
{HeavyObject o[2];
return o[index];
}
// call
HeavyObject o = dummy(1);
运行后的后果为
Constructor
Constructor
Copy Constructor
Destructor
Destructor
Destructor
从运行的后果能够看到没有做 RVO 优化,此时调用了拷贝构造函数。
从上述三种实现形式能够看到,如果你的函数实现性能比拟繁多,比方只会对一个对象进行操作并返回时,编译器会进行 RVO 优化;如果函数实现比较复杂,可能会波及操作多个对象并不确定返回哪个对象时,编译器将不做 RVO 优化,此时函数返回时会调用类的拷贝构造函数。
然而,当只存在一个本地对象时,编译器肯定会做 RVO 优化吗?
- 第四种应用形式
==========
HeavyObject func()
{return std::move(HeavyObject());
}
// call
HeavyObject o = func();
理论运行输入的后果是
Constructor
Move Constructor
Destructor
Destructor
上述的函数实现间接返回长期对象的右值援用,从理论的运行后果来看调用了 Move 构造函数,与第一种应用形式运行的后果显著不同,并不是我冀望的只调用一次构造函数和析构函数,也就是说编译器没有做 RVO。
- 第五种应用形式
==========
HeavyObject func()
{
HeavyObject o;
return static_cast<HeavyObject&>(o);
}
// call
HeavyObject o = func();
理论运行输入的后果是
Constructor
Copy Constructor
Destructor
Destructor
上述的函数实现间接返回本地对象的援用,理论运行后果依然调用了拷贝构造函数,并不是冀望的只调用一次结构和析构函数,也就是说编译器并没有做 RVO。
从上述两种应用形式能够看到,当返回一个对象时且对象类型与返回类型不统一时,编译器将不做 RVO。实际上 C ++ 规范文档中有如下形容:
in a return statement in a function with a class return type, when the expression is the name of a non-volatile automatic object (other than a function or catch-clause parameter) with the same cv-unqualified type as the function return type, the copy/move operation can be omitted by constructing the automatic object directly into the function’s return value
总结
- 两种 style 代码的性能可能会不一样,当你十分确定你的代码的开发环境及编译器的反对个性如 RVO,以及使用者的接入环境时,倡议应用 style 1,否则倡议应用 style 2
- RVO 的编译器优化个性须要绝对比拟严格的限度,应用 style 1 时,较简单的函数实现可能并不会如你冀望的应用 RVO 优化
作者:lifesider
原文链接
本文为阿里云原创内容,未经容许不得转载