c++ - pIter != cont.end() 在 for 循环中的性能

标签 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 "";
}

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

For most containers (including list), calling end() returns a temporary object that must be constructed and destroyed. Because the value will not change, recomputing (and reconstructing and redestroying) it on every loop iteration is both needlessly inefficient and unaesthetic. The value should be computed only once, stored in a local object, and reused.

他建议替换为以下内容:

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 .

最佳答案

UPD:除非我弄错了,否则您所说的这本书已于 1999 年出版。那是 14 年前的事了,在现代编程中,14 年是很多时间。许多在 1999 年很好且可靠的建议现在可能已经完全过时了。虽然我的回答是关于单一编译器和单一平台,但也有一个更笼统的想法。

关心额外的变量、重用琐碎方法的返回值以及旧 C++ 的类似技巧是向 1990 年代 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 <_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   

查看位于 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 <_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   

循环的代码类似,就是:

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

但是循环周围有很多额外的东西。显然,额外的代码让事情变得更糟了。

关于c++ - pIter != cont.end() 在 for 循环中的性能,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/15433381/

相关文章:

c++ - 取消卡在 epoll_wait 上的线程

android - Qt5:Android 上的原生外观小部件

c++ - 用户定义的重载运算符 * 与 std::chrono::duration

c++ - 为什么我不能通过 std::tuple 获得保证的复制省略?

python - Pandas to_sql() 性能 - 为什么这么慢?

C++从字符串指针解析int

javascript - 如何使用 jQuery 从 Web Worker Thread 中解析 XML

php - Symfony2 性能的奏鸣曲管理员

c++ - 声明具有特定大小并插入元素的 vector<vector<pair<int, int>>?

c++ - 是否可以出于性能目的创建默认初始化的类 std::is_trivial