2013-03-15 40 views
18

最近我正在通過Herb Sutter的「Exceptional C++」,並且我對他在第6項 - 臨時對象中給出的一個特殊建議深表懷疑。for循環中pIter!= cont.end()的性能

他提供找到下面的代碼不必要的臨時對象:

string FindAddr(list<Employee> emps, string name) 
{ 
    for (list<Employee>::iterator i = emps.begin(); i != emps.end(); i++) 
    { 
    if(*i == name) 
    { 
     return i->addr; 
    } 
    } 
    return ""; 
} 

作爲例子之一,他建議在循環之前預先計算的emps.end()價值,因爲沒有對每一個創建的臨時對象迭代:

對於大多數容器(包括列表),調用結束()返回一個 臨時對象,必須構造和銷燬。因爲 的值不會改變,所以在每次循環迭代中重新計算(並重建並重新銷燬)它都是不必要的,並且效率低下且不美觀。該值應該只計算一次, 存儲在本地對象中,並重新使用。

他並且建議通過以下替換:

list<Employee>::const_iterator end(emps.end()); 
for (list<Employee>::const_iterator i = emps.begin(); i != end; ++i) 

對於我來說,這是不必要的併發症。即使用簡潔的auto來代替醜陋的類型聲明,他仍然得到兩行代碼而不是一行代碼。更有甚者,他在外部範圍內有這個end變量。

我確信現代編譯器會優化這段代碼,因爲我實際上在這裏使用const_iterator,並且很容易檢查循環內容是否以某種方式訪問​​容器。編譯器在過去的13年中變得更聰明,對吧?

無論如何,在大多數情況下,我會更喜歡i != emps.end()的第一個版本,我並不太在意性能。但我想確切地知道,這是否是一種我可以依靠編譯器優化的構造?

更新

感謝如何使這個無用的代碼更好您的建議。請注意,我的問題是關於編譯器,而不是編程技術。現在唯一相關的答案是從NPEEllioh

+0

是的,這是真的,因爲這是一個發現臨時的例子。我在問這個例子的特定部分。 – Mikhail 2013-03-15 13:14:54

+0

不要試圖編寫儘可能快的代碼,而是編寫易於閱讀和易於理解的代碼。除非真的需要優化,否則不要浪費時間來優化代碼。 – LihO 2013-03-15 13:22:44

+0

@LihO是的,我完全同意你的看法。看到我的最後一段。我的問題不在於此。 – Mikhail 2013-03-15 13:26:03

回答

8

UPD:你說的這本書已經在1999年出版了,除非我錯了。那是14年前的事情,而在現代節目中,14年時間很長。1999年的許多建議很好,可靠,現在可能已經完全過時了。儘管我的答案是關於單個編譯器和單個平臺,但還有一個更普遍的想法。

關注額外的變量,重新使用平凡方法的返回值和類似的舊C++技巧是退步到20世紀90年代的C++。像end()這樣的簡單方法應該很好地內聯,並且內聯的結果應該作爲它被調用的代碼的一部分進行優化。 99%的情況下不需要手動操作,例如創建一個end變量。這樣的事情只有在以下情況下才能完成:

  1. 您知道在某些編譯器/平臺上應該運行的代碼沒有很好地優化。
  2. 它已成爲您程序中的瓶頸(「避免過早優化」)。

我看了由64位G ++生成的內容:

gcc version 4.6.3 20120918 (prerelease) (Ubuntu/Linaro 4.6.3-10ubuntu1) 

起初我以爲跟在它的優化應該是確定,應該有兩個版本之間沒有區別。但看起來很奇怪:你認爲非最佳版本實際上更好。我認爲,道德是:沒有理由嘗試比編譯器更智能。讓我們看看兩個版本。

#include <list> 

using namespace std; 

int main() { 
    list<char> l; 
    l.push_back('a'); 

    for(list<char>::iterator i=l.begin(); i != l.end(); i++) 
     ; 

    return 0; 
} 

int main1() { 
    list<char> l; 
    l.push_back('a'); 
    list<char>::iterator e=l.end(); 
    for(list<char>::iterator i=l.begin(); i != e; i++) 
     ; 

    return 0; 
} 

那麼我們就應該在優化編譯這個(我用的64位g++,你可以試試你的編譯器)和拆卸mainmain1

main

(gdb) disas main 
Dump of assembler code for function main(): 
    0x0000000000400650 <+0>: push %rbx 
    0x0000000000400651 <+1>: mov $0x18,%edi 
    0x0000000000400656 <+6>: sub $0x20,%rsp 
    0x000000000040065a <+10>: lea 0x10(%rsp),%rbx 
    0x000000000040065f <+15>: mov %rbx,0x10(%rsp) 
    0x0000000000400664 <+20>: mov %rbx,0x18(%rsp) 
    0x0000000000400669 <+25>: callq 0x400630 <[email protected]> 
    0x000000000040066e <+30>: cmp $0xfffffffffffffff0,%rax 
    0x0000000000400672 <+34>: je  0x400678 <main()+40> 
    0x0000000000400674 <+36>: movb $0x61,0x10(%rax) 
    0x0000000000400678 <+40>: mov %rax,%rdi 
    0x000000000040067b <+43>: mov %rbx,%rsi 
    0x000000000040067e <+46>: callq 0x400610 <_ZNSt8__detail15_List_node_base7_M_hoo[email protected]> 
    0x0000000000400683 <+51>: mov 0x10(%rsp),%rax 
    0x0000000000400688 <+56>: cmp %rbx,%rax 
    0x000000000040068b <+59>: je  0x400698 <main()+72> 
    0x000000000040068d <+61>: nopl (%rax) 
    0x0000000000400690 <+64>: mov (%rax),%rax 
    0x0000000000400693 <+67>: cmp %rbx,%rax 
    0x0000000000400696 <+70>: jne 0x400690 <main()+64> 
    0x0000000000400698 <+72>: mov %rbx,%rdi 
    0x000000000040069b <+75>: callq 0x400840 <std::list<char, std::allocator<char> >::~list()> 
    0x00000000004006a0 <+80>: add $0x20,%rsp 
    0x00000000004006a4 <+84>: xor %eax,%eax 
    0x00000000004006a6 <+86>: pop %rbx 
    0x00000000004006a7 <+87>: retq 

看在位於0x0000000000400683-0x000000000040068b的命令處。這就是循環體,它似乎是完美優化:

0x0000000000400690 <+64>: mov (%rax),%rax 
    0x0000000000400693 <+67>: cmp %rbx,%rax 
    0x0000000000400696 <+70>: jne 0x400690 <main()+64> 

對於main1

(gdb) disas main1 
Dump of assembler code for function main1(): 
    0x00000000004007b0 <+0>: push %rbp 
    0x00000000004007b1 <+1>: mov $0x18,%edi 
    0x00000000004007b6 <+6>: push %rbx 
    0x00000000004007b7 <+7>: sub $0x18,%rsp 
    0x00000000004007bb <+11>: mov %rsp,%rbx 
    0x00000000004007be <+14>: mov %rsp,(%rsp) 
    0x00000000004007c2 <+18>: mov %rsp,0x8(%rsp) 
    0x00000000004007c7 <+23>: callq 0x400630 <[email protected]> 
    0x00000000004007cc <+28>: cmp $0xfffffffffffffff0,%rax 
    0x00000000004007d0 <+32>: je  0x4007d6 <main1()+38> 
    0x00000000004007d2 <+34>: movb $0x61,0x10(%rax) 
    0x00000000004007d6 <+38>: mov %rax,%rdi 
    0x00000000004007d9 <+41>: mov %rsp,%rsi 
    0x00000000004007dc <+44>: callq 0x400610 <[email protected]> 
    0x00000000004007e1 <+49>: mov (%rsp),%rdi 
    0x00000000004007e5 <+53>: cmp %rbx,%rdi 
    0x00000000004007e8 <+56>: je  0x400818 <main1()+104> 
    0x00000000004007ea <+58>: mov %rdi,%rax 
    0x00000000004007ed <+61>: nopl (%rax) 
    0x00000000004007f0 <+64>: mov (%rax),%rax 
    0x00000000004007f3 <+67>: cmp %rbx,%rax 
    0x00000000004007f6 <+70>: jne 0x4007f0 <main1()+64> 
    0x00000000004007f8 <+72>: mov (%rdi),%rbp 
    0x00000000004007fb <+75>: callq 0x4005f0 <[email protected]> 
    0x0000000000400800 <+80>: cmp %rbx,%rbp 
    0x0000000000400803 <+83>: je  0x400818 <main1()+104> 
    0x0000000000400805 <+85>: nopl (%rax) 
    0x0000000000400808 <+88>: mov %rbp,%rdi 
    0x000000000040080b <+91>: mov (%rdi),%rbp 
    0x000000000040080e <+94>: callq 0x4005f0 <[email protected]> 
    0x0000000000400813 <+99>: cmp %rbx,%rbp 
    0x0000000000400816 <+102>: jne 0x400808 <main1()+88> 
    0x0000000000400818 <+104>: add $0x18,%rsp 
    0x000000000040081c <+108>: xor %eax,%eax 
    0x000000000040081e <+110>: pop %rbx 
    0x000000000040081f <+111>: pop %rbp 
    0x0000000000400820 <+112>: retq 

的循環中的代碼是相似的,那就是:

0x00000000004007f0 <+64>: mov (%rax),%rax 
    0x00000000004007f3 <+67>: cmp %rbx,%rax 
    0x00000000004007f6 <+70>: jne 0x4007f0 <main1()+64> 

但有循環周圍有很多額外的東西。顯然,額外的代碼使事情變得更糟糕。

+0

哇,太棒了!你能否對其他容器做同樣的檢查,至少對於'std :: vector'好嗎? :) – Mikhail 2013-03-15 13:52:02

+0

對於向量,我發現main()和main1()之間沒有區別,在這兩種情況下,優化程序完全消除了空循環。 – Ellioh 2013-03-15 14:02:00

+0

是的,這就是爲什麼我認爲這個問題有點棘手,有點模糊。 – Mikhail 2013-03-15 14:07:46

2

像vector這樣的容器返回變量,它存儲指向end()調用的指針,該指針用於優化。如果你寫的容器,它確實對end()調用一些查詢等考慮寫

for (list<Employee>::const_iterator i = emps.begin(), end = emps.end(); i != end; ++i) 
{ 
... 
} 

速度

3

幾件事情......第一個就是建立一個迭代的一般費用(在發佈模式下,未經檢查的分配器)是最小的。它們通常是圍繞指針的包裝。使用檢查分配器(VS中的默認值),您可能會有一些成本,但是如果您確實需要性能,那麼在使用未經檢查的分配器重新測試之後。

代碼不一定是醜如您發佈的內容:

for (list<Employee>::const_iterator it=emps.begin(), end=emps.end(); 
            it != end; ++it) 

您是否想使用一種或另一種方法應該是在什麼樣的操作方面的主要決策都被應用到容器。如果容器可能正在改變它的大小,那麼您可能需要在每次迭代中重新計算迭代器end。如果沒有,您可以預先計算一次,然後像上面的代碼一樣重新使用。

+0

想到隱藏''end''''聲明,謝謝!不幸的是,這不是我的問題的答案。 – Mikhail 2013-03-15 13:37:33

+0

@Mikhail:那麼我錯過了什麼問題......答案中的第一段討論了複製的可能成本,第二個問題是代碼中的* ugliness *。第三個提供了成本不同的基本原理...... – 2013-03-15 14:23:54

+0

然後,[Jerry Coffin here](http://stackoverflow.com/a/15435579/36565)的一些測試似乎表明編譯器已優化並調用'end()'成員函數只有一次。 – 2013-03-15 19:54:19

2

如果你真的需要的性能,你讓你閃亮的新的C++編譯器11寫吧:

for (const auto &i : emps) { 
    /* ... */ 
} 

是的,這是舌頭在臉頰(排序)。 Herb的例子現在已經過時了。但是由於你的編譯器還不支持它,讓我們來看看真正的問題:

這是一種我可以依靠編譯器優化的構造嗎?

我的經驗法則是編譯器編寫者比我更聰明。我不能依靠編譯器來優化任何一段代碼,因爲它可能會選擇優化其他影響更大的其他。唯一可以肯定的方法是試用編譯器您的系統,看看會發生什麼。檢查您的分析器結果。如果對.end()的呼叫不起作用,請將其保存在單獨的變量中。否則,不要擔心。

+0

我喜歡range-fors,但是Visual Studio還沒有:( – Mikhail 2013-03-15 13:22:38

+0

我認爲他們解決了錯誤的問題;使用'std :: for_each'。 – 2013-03-15 13:26:50

+0

@Mikhail:基於範圍for循環是許多新增和增強中的一個功能[Visual Studio 2012](http://msdn.microsoft.com/en-us/library/vstudio/hh409293.aspx)。 – Blastfurnace 2013-03-15 17:47:30

8

我編譯使用g++ 4.7.2-O3 -std=c++11以下略哈克的代碼,並得到了兩個功能相同的組件:

#include <list> 
#include <string> 

using namespace std; 

struct Employee: public string { string addr; }; 

string FindAddr1(list<Employee> emps, string name) 
{ 
    for (list<Employee>::const_iterator i = emps.begin(); i != emps.end(); i++) 
    { 
    if(*i == name) 
    { 
     return i->addr; 
    } 
    } 
    return ""; 
} 

string FindAddr2(list<Employee> emps, string name) 
{ 
    list<Employee>::const_iterator end(emps.end()); 
    for (list<Employee>::const_iterator i = emps.begin(); i != end; i++) 
    { 
    if(*i == name) 
    { 
     return i->addr; 
    } 
    } 
    return ""; 
} 

在任何情況下,我覺得這兩個版本之間的選擇應主要基於可讀性的理由。沒有分析數據,這樣的微觀優化看起來爲時過早。

+0

這就是我所要求的!但是我更喜歡編譯器開發人員的一些評論,因爲這是一個模型和簡單的例子,誰知道編譯器將決定更多複雜的情況? – Mikhail 2013-03-15 13:24:49

+0

這個答案只涉及一個沒有安全迭代器的標準庫的具體實現,可以用隨VS發佈的Dinkumware標準庫一起試驗,測試結果會有所不同(迭代器不是t圍繞指針的薄包裝,但會執行更多邏輯,存儲足夠的信息來跟蹤迭代器是否在以後失效)。 – 2013-03-15 14:26:06

0

使用std algorithms

他是對的,調用end可以實例化並銷燬臨時對象,這通常是不好的。

當然,編譯器可以在很多情況下優化它。

有一個更好,更強大的解決方案:封裝您的循環

你給出的例子實際上是std::find,給出或取回值。許多其他循環也有std算法,或者至少有類似的東西可以適應 - 例如,我的實用程序庫具有transform_if實現。

因此,隱藏函數中的循環並採取const&end。與您的示例相同,但要乾淨得多。

+0

我明白這一點。當然,我不會爲自己編寫這樣的代碼,我會像你說的那樣使用'std :: find'。 – Mikhail 2013-03-15 13:31:18

+0

@Mikhail我認爲這是更一般的解決方案;把一個'const'帶到違規的臨時。 – 2013-03-15 13:33:23

4

與流行的看法相反,我沒有看到VC++和gcc在這方面的區別。我用g ++ 4.7.2和MS C++ 17(又名VC++ 2012)做了快速檢查。

在這兩種情況下,我比較了代碼作爲問題產生(與添加到讓它編譯頭和這樣)的代碼,爲以下代碼:

string FindAddr(list<Employee> emps, string name) 
{ 
    auto end = emps.end(); 
    for (list<Employee>::iterator i = emps.begin(); i != end; i++) 
    { 
     if(*i == name) 
     { 
      return i->addr; 
     } 
    } 
    return ""; 
} 

在這兩種情況下,結果是基本上兩段代碼相同。 VC++在代碼中包含行號註釋,由於額外的行而改變,但這是唯一的區別。用g ++輸出文件是相同的。

這樣做與std::vector相同,而不是std::list,給出了幾乎相同的結果 - 無顯着差異。出於某種原因,g ++確實將一條指令的操作數順序從cmp esi, DWORD PTR [eax+4]切換爲cmp DWORD PTR [eax+4], esi,但是(再次)這是完全不相關的。底線:不,你不可能用現代編譯器手動將代碼從循環中提取出來(至少啓用了優化 - 我使用的是VC++和/O3和g ++;優化關閉優化似乎對我來說毫無意義)。

+0

這是與檢查迭代器或不安全的迭代器?此外,如果列表通過引用傳遞,會發生什麼? – 2013-03-15 15:31:22

+1

@DavidRodríguez-dribeas:檢查(我相信 - 我沒有做任何事情來禁用它們)。 – 2013-03-15 15:32:10

+1

@DavidRodríguez-dribeas:通過引用傳遞似乎也沒有任何區別。 – 2013-03-15 15:53:31