震惊!接受这4个小建议可以使你的C++程序性能提升80%!

  • 开宗明义,本文是标题党。《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
  • 可以看到其结果通swap大法一样。

建议三:使用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四种无序容器。因为这些容器中的元素不需要排序,所以他们的插入等操作的效率更高一些。
  • 无序容器内部使用哈希表来组织元素。通过哈希函数和关键字类型的==运算符来实现元素的快速操作。
  • 对于基本类型,我们可以像使用有序容器一样使用无序容器。对于自定义类型的结构体,就需要提供哈希函数和重载==运算符。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,384评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,845评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,148评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,640评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,731评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,712评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,703评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,473评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,915评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,227评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,384评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,063评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,706评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,302评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,531评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,321评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,248评论 2 352

推荐阅读更多精彩内容