for循环中pIter!= cont.end()的性能

Mik*_*ail 22 c++ performance stl compiler-optimization temporary-objects

我最近通过Herb Sutter获得了"Exceptional C++",我对他在第6项 - 临时对象中提出的特别建议表示严重怀疑.

他提供了在以下代码中查找不必要的临时对象:

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 "";
}
Run Code Online (Sandbox Code Playgroud)

作为示例之一,他建议预先计算emps.end()循环之前的值,因为每次迭代都会创建一个临时对象:

对于大多数容器(包括列表),调用end()返回一个必须构造和销毁的临时对象.因为值不会改变,所以在每次循环迭代中重新计算(并重建和重新描述)它都是不必要的低效和不美观的.该值应仅计算一次,存储在本地对象中,然后重复使用.

他建议用以下内容代替:

list<Employee>::const_iterator end(emps.end());
for (list<Employee>::const_iterator i = emps.begin(); i != end; ++i)
Run Code Online (Sandbox Code Playgroud)

对我来说,这是不必要的并发症.即使用compact替换丑陋的类型声明auto,他仍然会获得两行代码而不是一行代码.更重要的是,他end在外部范围内有这个变量.

我确信现代编译器无论如何都会优化这段代码,因为我实际上const_iterator在这里使用并且很容易检查循环内容是否以某种方式访问​​容器.编译器在过去的13年里变得更聪明,对吧?

无论如何,i != emps.end()在大多数情况下,我更喜欢第一个版本,我不太担心性能.但我想知道,这是否是一种我可以依靠编译器进行优化的结构?

更新

感谢您就如何更好地制作这些无用的代码提出建议.请注意,我的问题是关于编译器,而不是编程技术.现在唯一相关的答案来自NPEEllioh.

Ell*_*ioh 11

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)
Run Code Online (Sandbox Code Playgroud)

最初我认为通过优化它应该没问题,两个版本之间应该没有区别.但看起来很奇怪:你认为非最佳的版本实际上更好.我认为,道德是:没有理由尝试比编译器更聪明.我们来看两个版本.

#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;
}
Run Code Online (Sandbox Code Playgroud)

然后我们应该通过优化来编译它(我使用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 <_Znwm@plt>
   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_hookEPS0_@plt>
   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   
Run Code Online (Sandbox Code Playgroud)

查看位于0x0000000000400683-0x000000000040068b的命令.这是循环体,它似乎完美优化:

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

用于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 <_Znwm@plt>
   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 <_ZNSt8__detail15_List_node_base7_M_hookEPS0_@plt>
   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 <_ZdlPv@plt>
   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 <_ZdlPv@plt>
   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   
Run Code Online (Sandbox Code Playgroud)

循环的代码类似,它是:

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

但循环周围还有很多额外的东西.显然,额外的代码使事情变得更糟糕.


NPE*_*NPE 8

我使用g++ 4.7.2with 编译了以下稍微hacky的代码-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 "";
}
Run Code Online (Sandbox Code Playgroud)

无论如何,我认为两个版本之间的选择应主要基于可读性.如果没有分析数据,像我这样的微优化看起来还为时过早.