- 开宗明义,本文是标题党。《More Effective C++》书中效率部分第一条就是80—20准则。说得是——大约 20%的代码使用了 80%的程序资源;大约 20%的代码耗用了大约 80%的运行时间;大约 20%的代码使用了 80%的内存。
- 我们真正想大幅度的提升程序性能需要借助程序分析器(profiler)寻找出程序的性能瓶颈,针对这个瓶颈进行代码层面,算法层面,架构层面等多方面的优化。
- 下面这些小建议主要是针对代码层面进行的优化。
建议一:如果元素个数确定,使用reserve函数来提前为vector分配对象内存空间。
- C++程序中最常使用的容器就是vector。作为一个动态管理分配内存的数组容器,其易用性深受C++程序员的喜爱。但是,因为动态管理分配内存,vector的性能也一直是被人吐槽最多的地方。
- 如果在vector塞入元素之前能确定需要塞入元素的个数,那么调用reserve函数提前分配对象的内存空间。但是不要直接使用带元素数量的初始化方式来初始化vector或者是调用resize函数。如果不确定vector的元素个数,那么直接正常使用即可。
- 下面我们来看看这三种方式对vector数组都做了些什么。
#include <iostream>
#include <vector>
class BigClass {
public:
BigClass() {
std::cout << "BigClass constructor" << std::endl;
}
};
int main() {
std::cout << "initialize vector with element number..." << std::endl;
std::vector<BigClass> vec1(10);
std::cout << "vec1 size:" << vec1.size() << std::endl << std::endl;
std::cout << "vector resize..." << std::endl;
std::vector<BigClass> vec2;
vec2.resize(10);
std::cout << "vec2 size:" << vec2.size() << std::endl << std::endl;
std::cout << "vector reserve..." << std::endl;
std::vector<BigClass> vec3;
vec3.reserve(10);
std::cout << "vec3 size:" << vec3.size() << std::endl << std::endl;
system("pause");
return 0;
}
initialize vector with element number...
BigClass constructor
BigClass constructor
BigClass constructor
BigClass constructor
BigClass constructor
BigClass constructor
BigClass constructor
BigClass constructor
BigClass constructor
BigClass constructor
vec1 size:10
vector resize...
BigClass constructor
BigClass constructor
BigClass constructor
BigClass constructor
BigClass constructor
BigClass constructor
BigClass constructor
BigClass constructor
BigClass constructor
BigClass constructor
vec2 size:10
vector reserve...
vec3 size:0
- 可以看到无论是使用带元素数量的初始化方式还是调用resize,都会调用存储类型的默认构造函数并改变size。一般情况下,这个通过默认构造函数生成的对象是没什么用处,最后都会在塞入数据的时候被覆盖掉。而且如果存储类型是个比较大的或者说是个构造函数比较复杂的类,那么这两种方式对于性能的浪费就很大了。
- 而reserve函数能够实现同resize函数一样的先分配指定个数元素的内存空间,但是不进行该对象的构造。即reserve只做纯粹的内存空间分配(只改变了它的capacity)。
- 而且我们提前使用reserve函数分配了内存空间也节省了系统为我们动态分配时所消耗的内存。
- 所以,使用vector时尽量用reserver函数来提高性能。
建议二:对于数据量比较大的vector,使用clear+shrink_to_fit函数来正确的释放内存。
- C++11的vector提供了shrink_to_fit函数来使容器降低其capacity和size匹配。对于已经存储了很大数据量的vector对象,我们可以使用clear+shrink_to_fit函数来替换原来的swap大法来正确的释放vector使用的内存。这种主动释放内存的操作对于程序的性能也是有一定提升的。
- 首先,我们知道当我们对一个vector调用clear函数的时候,实际上只是清理了vector中的元素,使得vector的size变成了0,但是它的capacity并没有变成0。也就是说vector所占用的内存并没有被释放掉,我们仍然可以通过某种方式获取到之前的元素。看下面代码。
#include<iostream>
#include<vector>
using namespace std;
int main() {
vector<int> v {1,2,3,4,5};
cout << "size:" << v.size() << endl;
cout << "capacity:" << v.capacity() << endl;
v.clear();
cout << "after clear size:" << v.size() << endl;
cout << "after clear capacity:" << v.capacity() << endl;
cout << "v[0] = " << *v.data() << endl;
system("pause");
return 0;
}
size:5
capacity:5
after clear size:0
after clear capacity:5
v[0] = 1
- 在C++11之前,我们一般使用swap大法来释放内存。用一个空的vector和需要清空的vector进行swap,这样原来的vector被空vector替换成空的,而之前的空vector被替换为原来vector中的内存,其作为临时变量在离开作用域后就会被自动析构,从而释放掉了原来vector中的内存。代码如下。
#include<iostream>
#include<vector>
using namespace std;
int main() {
vector<int> v{ 1,2,3,4,5 };
cout << "size:" << v.size() << endl;
cout << "capacity:" << v.capacity() << endl;
v.swap(vector<int>());
cout << "after swap size:" << v.size() << endl;
cout << "after swap capacity:" << v.capacity() << endl;
system("pause");
return 0;
}
size:5
capacity:5
after swap size:0
after swap capacity:0
- 可以看到swap大法可以达到释放内存的需求。但是,这种语法多少会显得有些怪异。而C++11为我们带来了vector的新函数shrink_to_fit,这样我们可以使用clear+shrink_to_fit的方法来达到和swap大法一样的效果,而且语法很清晰。使用示例如下。
#include<iostream>
#include<vector>
using namespace std;
int main() {
vector<int> v{ 1,2,3,4,5 };
cout << "size:" << v.size() << endl;
cout << "capacity:" << v.capacity() << endl;
v.clear();
v.shrink_to_fit();
cout << "after clear+shrink_to_fit size:" << v.size() << endl;
cout << "after clear+shrink_to_fit capacity:" << v.capacity() << endl;
system("pause");
return 0;
}
size:5
capacity:5
after clear+shrink_to_fit size:0
after clear+shrink_to_fit capacity:0
建议三:使用emplace_back替代push_back减少内存拷贝和移动。
- C++11中对所有的标准容器(除了array)都增加了emplace、emplace_front、emplace_back等来替代原来的insert、push_front、push_back等插入元素的操作。进一步提升容器插入元素的性能。相比于原来的push_back,emplace_back能通过直接通过参数就地的在内存中构造元素对象,而不需要执行拷贝或者移动内存的操作。所以,我们在大部分情况下,都可以使用emplace系列的函数来替代insert或push系列的函数来提升程序的性能。
- 我们直接看例子。
#include <vector>
#include <string>
#include <iostream>
#include <map>
struct Person {
std::string name;
std::string country;
int year;
Person(std::string p_name, std::string p_country, int p_year)
: name(std::move(p_name)), country(std::move(p_country)), year(p_year) {
std::cout << "I am being constructed." << std::endl;
}
Person(Person&& other)
: name(std::move(other.name)), country(std::move(other.country)), year(other.year) {
std::cout << "I am being moved." << std::endl;
}
};
int main() {
std::map<int, Person> m;
std::cout << "map insert..." << std::endl;
m.insert(std::make_pair(23333, Person("kk", "china", 9021)));
std::cout << "map emplace..." << std::endl;
m.emplace(23333, Person("kk", "china", 9021));
std::vector<Person> v;
std::cout << "vector push_back..." << std::endl;
v.push_back(Person("kk", "china", 9021));
std::vector<Person> v1;
std::cout << "vector emplace_back..." << std::endl;
v1.emplace_back("kk", "china", 9021);
system("pause");
return 0;
}
map insert...
I am being constructed.
I am being moved.
I am being moved.
map emplace...
I am being constructed.
I am being moved.
vector push_back...
I am being constructed.
I am being moved.
vector emplace_back...
I am being constructed.
- 从运行结果可以看出无论是map还是vector使用emplace系列的函数都少了一次内存移动的调用。而且emplace系列的函数还能直接通过参数就地构造(需要有对应的构造函数),这样代码也可以少写点。
建议四:在没有排序需求时,使用无序容器(unordered container)替代有序容器。
- C++11提供了unordered_map、unordered_multimap、unordered_set和unordered_multiset四种无序容器。因为这些容器中的元素不需要排序,所以他们的插入等操作的效率更高一些。
- 无序容器内部使用哈希表来组织元素。通过哈希函数和关键字类型的==运算符来实现元素的快速操作。
- 对于基本类型,我们可以像使用有序容器一样使用无序容器。对于自定义类型的结构体,就需要提供哈希函数和重载==运算符。