PITER的性能!= cont.end()在for循环(Performance of pIter !

2019-07-21 16:53发布

我是通过“特殊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()在循环之前,由于在每个迭代上创建的临时对象:

对于大多数的容器(包括列表),主叫端()返回,必须构造和破坏临时对象。 因为该值将不会改变,重新计算(以及重建和redestroying)在每次循环迭代它既是不必要低效和不美观的。 该值应被计算一次,存储在本地对象,并重复使用。

他建议通过以下替换:

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()在大多数情况下,如果我不是这么多的表现担心。 但我想肯定知道,这是否是一种建筑的,我可以依靠编译器优化?

更新

感谢您对如何使这个无用的代码更好您的建议。 请注意,我的问题是有关编译器,而不是编程技术。 现在唯一的相关解答来自NPE和Ellioh 。

Answer 1:

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 <_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>

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



Answer 2:

我已经编译使用以下略哈克代码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 "";
}

在任何情况下,我觉得这两个版本之间的选择应主要基于可读性的理由。 如果没有分析数据,微观优化像这样我看为时过早。



Answer 3:

流行的看法相反,我没有看到VC ++和海湾合作委员会在这方面的任何差异。 我做了快速检查既克++ 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 ,给了几乎相同的结果-没有显著差异。 出于某种原因,克++做切换操作数的顺序为一个指令,从cmp esi, DWORD PTR [eax+4]cmp DWORD PTR [eax+4], esi ,但(再次),这是完全不相关的。

底线:不,你不可能从一个现代的编译器(至少在启用优化的手动起重代码圈外获得任何东西-我用/O2b2用VC ++和/O3与G ++;具有比较优化关闭优化似乎很没有意义对我来说)。



Answer 4:

一对夫妇的事情......第一,是普通建筑的迭代器(在Release模式,选中分配器)的成本是最小的。 它们通常是围绕一个指针包装。 随着检查分配器(VS中的默认值),你可能有一定的成本,但如果你真的需要的性能,测试后重建时取消选中分配器。

代码不需要像你贴什么丑:

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

您是否想主要决定使用一种或另一种方法应该是在什么都被应用到集装箱业务方面。 如果容器可能会改变它的大小,那么你可能需要重新计算end在每个迭代的迭代器。 如果没有,你可以预先计算一次,重用如在上面的代码。



Answer 5:

像载体的容器返回变量,其存储指向结束时,就end()调用,该优化。 如果你写的容器,它确实在某些查询,等end()调用考虑写

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

速度



Answer 6:

如果你真的需要的性能,你让你闪亮的新的C ++编译器11写吧:

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

是的,这是舌头在脸颊(排序)。 这里香草的例子是已经过时。 但由于你的编译器不支持它,让我们开始真正的问题:

这是一种建筑的,我可以依靠编译器优化?

我的经验法则是,编译器编写者的方式比我聪明。 我不能依赖于编译器优化任何一段代码,因为它可能会选择优化别的东西 ,有一个更大的影响。 肯定知道的唯一方法是尝试在你的编译器这两种方法系统上,并看看会发生什么。 检查你的分析器的结果。 如果调用.end()伸出,将其保存在一个单独的变量。 否则,不用担心了。



Answer 7:

使用std算法

他是对的,当然, 主叫end可以实例化和销毁临时对象,这是普遍不好。

当然,编译器可以在很多情况下,优化这个了。

有一个更好的,更可靠的解决方案: 封装你的循环

你给的例子实际上std::find ,或采取的返回值。 许多其他循环也有std的算法,或者至少很相似,你可以适应的东西-我的工具库具有transform_if实现,例如。

所以,藏在一个函数循环,并采取const&end 。 同样的定位点作为你的榜样,但要干净多了。



文章来源: Performance of pIter != cont.end() in for loop