zoukankan      html  css  js  c++  java
  • C++的高效从何而来(二)

    之前就写过一篇博客《C++的高效从何而来》,分析C++中效率问题。最近在Herb Sutter(C++标准委员会的chair)的GotW中看到了这篇文章GotW #2: Temporary Objects (5/10),主要是讲C++中临时对象的问题,文章给出了一段代码,问读者有多少处地方产生了不必要的临时对象。代码如下:

    string find_addr( list<employee> emps, string name ) {
        for( auto i = begin(emps); i != end(emps); i++ ) {
            if( *i == name ) {
                return i->addr;
            }
        }
        return "";
    }

    这段代码的作用是在emps这个list中寻找名字为name的那个employee。具体答案我们在这边不做太多讨论,有兴趣的可以自己想想。

    我看到这个函数以后,第一反应是:这样的实现是不是最好的?

    我们知道C++里面要完成这样一个查找有很多方式,我自己想到了下面着一些(我在代码中用的是vector):

    1、最直观的for循环

    // original for loop
    string find_addr_01(const vector<Employee>& emps, const string& name)
    {
        auto emps_end = end(emps);
        for (auto iter = begin(emps); iter != emps_end; ++iter) {
            if (iter->name_ == name) {
                return iter->addr_;
            }
        }
    return ""; }

    这段代码其实是我自己对Herb Sutter那段代码的一个改进版,消除了不必要的临时变量。

    2、C++11中引入了range-based for循环,用法和Java中// Range-based for loop(since C++11)

    string find_addr_02(const vector<Employee>& emps, const string& name)
    {
        for (const auto& employee : emps) {
            if (employee.name_ == name) {
                return employee.addr_;
            }
        }
    return ""; }

    这段代码相对于find_addr_01没有什么新东西,语法简单而已。

    3、STL算法find_if和lambda表达式

    // find_if with lambda expression
    string find_addr_03(const vector<Employee>& emps, const string& name){
        auto& iter = find_if(begin(emps), end(emps), [&](const Employee& employee) -> bool {
            return employee.name_ == name;
        });
    return iter != end(emps) ? iter->addr_ : "";
    }

    这段代码将find_if算法和lambda表达式结合起来,比较直观和优雅,符合Modern C++的风格。

    4、find_if的手动实现版

    了解STL的人应该知道find_if的内部实现,其实很简单:

    template <class InputIterator, class Predicate> 
    InputIterator find_if(InputIterator first, InputIterator last, Predicate pred) {
        while(first != last && !pred(*first)) ++first;
        return first;
    }

    我自己也可以手动实现,还省了一次函数调用:

    // original for loop without if inside while
    string find_addr_04(const vector<Employee>& emps, const string& name)
    {
        auto iter = begin(emps);
        auto emps_end = end(emps);
        while (iter != emps_end && iter->name_ != name) ++iter;
    return iter != emps_end ? iter->addr_ : ""; }

    这段代码和find_addr_01中的差别在于,把循环中的if语句的判断提到循环中来了。

    5、独具C++特色的function object

    // function object
    struct EmpCompare {
        EmpCompare(const string& name) : name_(name)
        {}
        bool operator()(const Employee& emp)
        {
            return emp.name_ == name_;
        }
        string name_;
    };
    
    // find_if with function object
    string find_addr_05(const vector<Employee>& emps, const string& name){
        auto& iter = find_if(begin(emps), end(emps), EmpCompare(name));
    return iter != end(emps) ? : iter->addr_ : ""; }

    这段代码和find_addr_03很像,只不过把find_if中的Predicate从lambda表达式换成了function object而已。

    6、最后一个是find_if和bind的综合

    bool emp_compare(const Employee& emp, const string& name)
    {
        return emp.name_ == name;
    }
    
    // find_if with bind and function pointer
    string find_addr_06(const vector<Employee>& emps, const string& name){
        auto& iter = find_if(begin(emps), end(emps), bind(emp_compare, placeholders::_1, name));
    return iter != end(emps) ? iter->addr_ : "";
    }

    bind是C++11新加入的adapter,作用相当于bind1st和bind2nd,但用途更广,支持绑定多个,placeholders::_1是一个占位符,表示待接受的参数。

    不知道大家感觉上面这6个函数,效率谁高谁低?

    废话少说,写测试代码,首先是一些辅助函数(这段代码的风格是模仿Milo Yip的):

    #define COUNT 10000        // loop count
    #define EMPS  10000        // emps size
    
    #define TIME(X) { \
        LARGE_INTEGER start, stop, freq; \
        QueryPerformanceCounter(&start); \
        {X;} \
        QueryPerformanceCounter(&stop); \
        QueryPerformanceFrequency(&freq); \
        double duration = (double)(stop.QuadPart - start.QuadPart) / (double)(freq.QuadPart); \
        cout << setw(10) << fixed << duration << " " << #X << endl; \
    }
    struct Employee {
        Employee(const string& name, const string& addr)
            : name_(name), addr_(addr)
        {}
        string name_;
        string addr_;
    };
    
    typedef string (*find_addr_func)(const vector<Employee>&, const string&);
    void test_average(const vector<Employee>& emps,  find_addr_func find_addr)
    {
        for (int i = 0; i < COUNT; i++) {
            string addr = find_addr(emps, "name5000");
            assert(addr == "addr5000");
        }
    }

    COUNT是循环次数,EMPS是员工的数目,TIME宏用来记录运行时间(较精确,仅在windows下有效,linux下可以用不是特别精确的clock_t来实现),test_average函数接受一个vector和一个find_addr_func类型的函数指针(通过typedef定义),分别去寻找名字为"name5000”的employee。

    main函数如下:

    int main()
    {
        vector<Employee> emps;
        emps.reserve(EMPS);
        for (int i = 0; i < EMPS; ++i) {
            stringstream ssname, ssaddr;
            ssname << "name" << i;
            ssaddr << "addr" << i;
            emps.push_back(Employee(ssname.str(), ssaddr.str()));
        }
        //srand(0);
        //random_shuffle(begin(emps), end(emps));
    
        TIME(test_average(emps, find_addr_01));
        TIME(test_average(emps, find_addr_02));
        TIME(test_average(emps, find_addr_03));
        TIME(test_average(emps, find_addr_04));
        TIME(test_average(emps, find_addr_05));
        TIME(test_average(emps, find_addr_06));
    
        return 0;
    }

    首先把vector初始化好,预留10000个空间,构造出“name0,addr0”到“name9999,addr9999”的Employee对象。大家可能看出来了,在调用find_addr_xx的时候,每次都是寻找中间的name5000这个employee。大家也可以通过random_shuffle来将vector中的对象随机打乱,不过这样对我们测试的影响不大。

    我的机器配置是i5-2400+8G,在VS2012 Update2中的运行结果:

    Debug(/Od):

    image

    Release(/O2):

    image

    大家可能会问,是我眼睛看花了吗?怎么相差几十倍啊!

    这也是我没有搞懂的一个问题,VC到底在Debug模式下做了什么东西,怎么会这么慢?

    在没有优化之前,find_if+lambda表达式,以及find_if+function object是效率最高的,但是优化以后就不一定了,所以我决定看看其它编译器的结果。

    使用MinGW4.8,Debug模式下(不开优化):

    image

    Release模式下(-O2):

    image

    MinGW给出的结果就比较靠谱了,优化效果也很明显,并且优化前后效率关系基本保持一致,比较利于我们的分析。

    首先,find_if+bind效率最低,原因是adapter从某种程度上阻止了内联(MinGW中没有加优化,时间几乎是其它的4倍),但编译器优化似乎做得也不错,最后也没有慢太多;

    其次,VC下手写的for循环总体比find_if要慢一些,但find_addr_04的效率在VC中是挺高的,说明是while循环中的if语句起了影响,使得CPU的分支预测更准确,提出来之后效率提高很明显;

    第三,VC下find_addr_04比find_if+lambda和find_if+function object要快,因为lambda表达式和function object都多了一个function object的构造和析构,MinGW中这一点体现不出来,不是很清楚原因,但是相差也不大;

    第四,MinGW秒杀VC,我真想不通微软在自己的操作系统上的编译器为什么还不如Linux上GCC移植过来的编译器产生的代码?

    结论:C++中实现循环的方式很多,之间的效率差别有各种原因(主要是内联和分支预测),但现在的C++编译器的优化能力已经是非常强悍了,效率上的差别已经不是那么明显了。反而最大的区别是体现在代码风格上,我个人比较喜欢find_if+lambda表达式,它是高效(lambda表达式容易内联)和直观(STL中的算法实现即高效又直观,从名字上就知道用途,不用自己写for循环,很不直观)的完美结合。不知道大家更喜欢哪种?

    Top
    收藏
    关注
    评论
  • 相关阅读:
    react开发环境准备
    react介绍
    课程大纲
    Proving Equivalences HDU-2767 (tarjan缩点)
    tarjan求强连通分量 + 缩点 + 求割点割边
    树的重心(性质+模版)
    Educational Codeforces Round 93 (Rated for Div. 2)(A B C D)
    Friend-Graph HDU
    Codeforces Round #665 (Div. 2) (A B C D E)
    Matrix Again HDU
  • 原文地址:https://www.cnblogs.com/codemood/p/3071795.html
Copyright © 2011-2022 走看看