网站商城系统,网站发布和推广,网站开发培训 从0,中国域名网官网前言
考虑存在这样一个类如HeavyObject#xff0c;其拷贝赋值操作比较耗时#xff0c;通常你在使用函数返回这个类的一个对象时会习惯使用哪一种方式#xff1f;或者会根据具体场景选择某一种方式#xff1f;
// style 1
HeavyObject func(Args param);// style 2
bool f…前言
考虑存在这样一个类如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内部使用clanglibc编译外部调用的编译环境为gccgnustl或者vc除了函数调用开销不用担心其它性能开销以及由于编译环境不同会崩溃问题。
因此这里我主要剖析一下style 1背后开发者需要关注的点。
RVO
RVO是Return Value Optimization的缩写即返回值优化NRVO就是具名的返回值优化为RVO的一个变种此特性从C11开始支持也就是说C98、C03都是没有将此优化特性写到标准中的不过少量编译器在开发过程中也会支持RVO优化如IBM Compiler比如微软是从Visual Studio 2010才开始支持的。
仍然以上述的HeavyObject类为例为了更清晰的了解编译器的行为这里实现了构造/析构及拷贝构造、赋值操作、右值构造函数如下
class HeavyObject
{
public:HeavyObject() { cout Constructor\n; }~HeavyObject() { cout Destructor\n; }HeavyObject(HeavyObject const) { cout Copy Constructor\n; }HeavyObject operator(HeavyObject const) { cout Assignment Operator\n; return *this; }HeavyObject(HeavyObject) { cout Move Constructor\n; }
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 %rbp100000f61: 48 89 e5 mov %rsp,%rbp100000f64: 48 83 ec 10 sub $0x10,%rsp100000f68: 48 89 f8 mov %rdi,%rax100000f6b: 48 89 45 f8 mov %rax,-0x8(%rbp)100000f6f: e8 0c 00 00 00 callq 100000f80 __ZN11HeavyObjectC1Ev100000f74: 48 8b 45 f8 mov -0x8(%rbp),%rax100000f78: 48 83 c4 10 add $0x10,%rsp100000f7c: 5d pop %rbp100000f7d: 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 %rbp100000f41: 48 89 e5 mov %rsp,%rbp100000f44: 48 83 ec 20 sub $0x20,%rsp100000f48: 48 89 f8 mov %rdi,%rax100000f4b: 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 __Z4funcv0x33100000f6a: 48 8b 7d f0 mov -0x10(%rbp),%rdi100000f6e: e8 2d 00 00 00 callq 100000fa0 __ZN11HeavyObjectD1Ev // HeavyObject::~HeavyObject()100000f73: 48 8b 45 e8 mov -0x18(%rbp),%rax100000f77: 48 83 c4 20 add $0x20,%rsp100000f7b: 5d pop %rbp100000f7c: 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_castHeavyObject(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 2RVO的编译器优化特性需要相对比较严格的限制使用style 1时较复杂的函数实现可能并不会如你期望的使用RVO优化
作者lifesider
原文链接
本文为阿里云原创内容未经允许不得转载