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/38882625/

相关文章:

performance - 计算每条指令的周期

c++ - 为什么关系运算符在 STL 字符串中重载为非成员函数?

c++ - 使用 C++ 只读取具有格式化和未格式化数据的文件中的格式化数据

c++ - 从临时流中提取字符时出现 GCC 编译器错误

php - 有什么方法可以在 PHP 中保持与 mysql 数据库的连接?

javascript - 调用 JavaScript/jQuery 函数的不同方法的性能

c++ - 为什么 bind1st 和 bind2nd 需要常量函数对象?

c++ - Microsoft C++ STL for Windows CE 的差异?

c++ - 是否可以从函数模板返回可变参数 lambda?

从 vector<pair<double*,pair<double*,int*>> 到数组的 C++ 转换