严正声明:本文系作者davidhopper原创,未经许可,不得转载。
C++11以及之后的C++14、17标准均提出一项编译优化技术:复制省略(copy elision,也称复制消除),另外还有RVO(return value optimization,返回值优化)或NRVO(named return value optimization,具名返回值优化)的提法,其实都是一回事。维基百科的Copy elision词条以及cppreference中的Copy elision词条均对该技术做了描述,但写得过于学术化,让人似懂非懂。RVO V.S. std::move写得不错,可以学习借鉴。这项优化技术的中心思想是:一个函数若直接返回类对象,一般会生成临时类对象变量,需多次调用拷贝构造函数(copy constructor)造成效率低下,编译器对此优化,省略其中的拷贝构造环节,达到提升效率的目的。
一、示例
下面来看一个具体示例。首先定义一个BigObject
类:
class BigObject {
public:
BigObject() : person_names_(std::vector<std::string>(1000000)) {
std::cout << "constructor. " << person_names_.size() << std::endl;
}
~BigObject() {
std::cout << "destructor. " << std::endl;
}
BigObject(const BigObject& other) {
person_names_ = other.person_names_;
std::cout << "copy constructor. " << person_names_.size() << std::endl;
}
private:
std::vector<std::string> person_names_;
};
其次,按照C++菜鸟的思路,定义一个直接返回BigObject
类对象的函数:
// 不管C++标准如何规定,强烈反对写这种没有一点C++味道的垃圾代码!!!
BigObject Foo() {
BigObject local_obj;
return local_obj;
}
最后,在main
函数中调用Foo
函数:
int main() {
BigObject obj = Foo();
return 0;
}
将上述代码全部放置于一个叫test.cpp
的文件,内容如下:
#include <iostream>
#include <string>
#include <vector>
class BigObject {
public:
BigObject() : person_names_(std::vector<std::string>(1000000)) {
std::cout << "constructor. " << person_names_.size() << std::endl;
}
~BigObject() {
std::cout << "destructor. " << std::endl;
}
BigObject(const BigObject& other) {
person_names_ = other.person_names_;
std::cout << "copy constructor. " << person_names_.size() << std::endl;
}
private:
std::vector<std::string> person_names_;
};
// 不管C++标准如何规定,强烈反对写这种没有一点C++味道的垃圾代码!!!
BigObject Foo() {
BigObject local_obj;
return local_obj;
}
int main() {
BigObject obj = Foo();
return 0;
}
GCC编译器默认打开“复制省略”优化选项,因此我们使用下述编译指令手动禁用该选项:
g++ -g -fno-elide-constructors -Wall -std=c++11 test.cpp -o test
执行生成的文件:
./test
结果如下:
constructor. 1000000
copy constructor. 1000000
destructor.
copy constructor. 1000000
destructor.
destructor.
可见,未开启“复制省略”优化选项时,直接返回BigObject
类对象,会造成额外调用两次拷贝构造函数。在我的Ubuntu 16.04
系统上,采用gcc 5.4.0
编译器,一个空std::string
类对象占用32字节的空间,调用一次BigObject
类的拷贝构造函数约消耗:(1000000 * 32) / (1024 * 1024) = 30.52 (MB)内存。若该函数在一个10000次的循环中调用,光是这种无谓的内存分配开销就将达到:10000 * 2 * 30.52 / 1024 = 596.1 (GB) 。不是说会一次占用这么多的内存,而是说内存空间会无谓地进行596.1GB的内存分配和释放操作,这是多大的空间浪费!又会造成多大的效率折扣!
如果使用GCC编译器默认提供的“复制省略”优化选项:
g++ -g -Wall -std=c++11 test.cpp -o test
执行生成的文件:
./test
结果如下:
constructor. 1000000
destructor.
彻底消除了拷贝构造函数的调用,执行效率大为提升!
二、结论
尽管C++11以上标准提出“复制省略(copy elision)”优化策略,并且GCC等主流编译器均支持该优化,但我强烈不建议使用该技术。这样写出来的代码还有C++的味道吗?如果C++可以直接返回类对象,那还要C++干什么?直接使用Python或者Matlab就行了。我们在使用C++语言时,必须牢记,返回值必须是int
、bool
、枚举或指针等轻量级原生(lightweight primitive)数据类型。如果确实需要返回大型数据,请使用引用或指针作为输出参数返回,而不是通过return
语句返回。