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循環,很不直觀)的完美結合。不知道大家更喜歡哪種?


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM