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

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

作者: 拉普拉斯妖kk | 来源:发表于2019-07-30 21:57 被阅读0次
    • 开宗明义,本文是标题党。《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四种无序容器。因为这些容器中的元素不需要排序,所以他们的插入等操作的效率更高一些。
    • 无序容器内部使用哈希表来组织元素。通过哈希函数和关键字类型的==运算符来实现元素的快速操作。
    • 对于基本类型,我们可以像使用有序容器一样使用无序容器。对于自定义类型的结构体,就需要提供哈希函数和重载==运算符。

    相关文章

      网友评论

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

          本文链接:https://www.haomeiwen.com/subject/jyworctx.html