STL find比手工制作的循环更好

时间:2011-01-02 23:02:06

标签: c++ stl assembly find performance

我有一些问题。给出以下C ++代码片段:

#include <boost/progress.hpp>

#include <vector>
#include <algorithm>
#include <numeric>
#include <iostream>

struct incrementor
{
  incrementor() : curr_() {}

  unsigned int operator()()
  { return curr_++; }

private:
  unsigned int curr_;
};

template<class Vec>
char const* value_found(Vec const& v, typename Vec::const_iterator i)
{
  return i==v.end() ? "no" : "yes";
}


template<class Vec>
typename Vec::const_iterator find1(Vec const& v, typename Vec::value_type val)
{
  return find(v.begin(), v.end(), val);
}


template<class Vec>
typename Vec::const_iterator find2(Vec const& v, typename Vec::value_type val)
{
  for(typename Vec::const_iterator i=v.begin(), end=v.end(); i<end; ++i)
    if(*i==val) return i;
  return v.end();
}

int main()
{
  using namespace std;
  typedef vector<unsigned int>::const_iterator iter;
  vector<unsigned int> vec;
  vec.reserve(10000000);

  boost::progress_timer pt;

  generate_n(back_inserter(vec), vec.capacity(), incrementor());
  //added this line, to avoid any doubts, that compiler is able to
  // guess the data is sorted
  random_shuffle(vec.begin(), vec.end());

  cout << "value generation required: " << pt.elapsed() << endl;

  double d;
  pt.restart();
  iter found=find1(vec, vec.capacity());
  d=pt.elapsed();
  cout << "first search required: " << d << endl;
  cout << "first search found value: " << value_found(vec, found)<< endl;


  pt.restart();
  found=find2(vec, vec.capacity());
  d=pt.elapsed();
  cout << "second search required: " << d << endl;
  cout << "second search found value: " << value_found(vec, found)<< endl;


  return 0;
}

在我的机器上(Intel i7,Windows Vista)STL查找(通过find1调用)运行速度比手工制作的循环快10倍(通过find2调用)。我首先想到Visual C ++执行某种矢量化(可能我在这里弄错了),但据我所知,程序集看起来不像它使用矢量化。为什么STL循环更快?手工制作的循环与STL-find主体的循环相同。

我被要求发布节目的输出。没有洗牌:

value generation required: 0.078
first search required: 0.008
first search found value: no
second search required: 0.098
second search found value: no

随机播放(缓存效果):

value generation required: 1.454
first search required: 0.009
first search found value: no
second search required: 0.044
second search found value: no

非常感谢,

dusha。

P.S。我返回迭代器并写出结果(找到与否),因为我想阻止编译器优化,它认为根本不需要循环。搜索到的值显然不在向量中。

P.P.S。我被要求发布为查找功能生成的程序集。这是:

found=find1(vec, vec.capacity());
001811D0  lea         eax,[esp+5Ch] 
001811D4  call        std::vector<unsigned int,std::allocator<unsigned int> >::capacity (1814D0h) 
001811D9  mov         esi,dword ptr [esp+60h] 
001811DD  mov         ecx,dword ptr [esp+64h] 
001811E1  cmp         esi,ecx 
001811E3  je          wmain+180h (1811F0h) 
001811E5  cmp         dword ptr [esi],eax 
001811E7  je          wmain+180h (1811F0h) 
001811E9  add         esi,4 
001811EC  cmp         esi,ecx 
001811EE  jne         wmain+175h (1811E5h) 



found=find2(vec, vec.capacity());
001812AE  lea         eax,[esp+5Ch] 
001812B2  call        std::vector<unsigned int,std::allocator<unsigned int> >::capacity (1814D0h) 
001812B7  mov         ecx,dword ptr [esp+60h] 
001812BB  mov         edx,dword ptr [esp+64h] 
001812BF  cmp         ecx,edx 
001812C1  je          wmain+262h (1812D2h) 
001812C3  cmp         dword ptr [ecx],eax 
001812C5  je          wmain+34Fh (1813BFh) 
001812CB  add         ecx,4 
001812CE  cmp         ecx,edx 
001812D0  jne         wmain+253h (1812C3h) 

find2使用ecx-register而不是esi。这两个寄存器有什么区别?是否可以认为esi假定指针正确对齐,从而带来额外的性能?

读取一些程序集引用ecx只是一个计数器,而esi是内存源。所以我认为STL算法知道Random Access Iterator正确对齐,因此使用内存指针。在非STL版本中,没有猜测对齐方式。我是对的吗?

6 个答案:

答案 0 :(得分:5)

Visual C ++的find算法使用未经检查的迭代器,而您的手写循环使用已检查的迭代器。

我的另一个猜测是,您在std::vector<t>::end()的循环的每次迭代中调用find2,而std::find只导致对开始和结束访问者的一次调用。 我是个白痴。

答案 1 :(得分:3)

确保在关闭checked iterators的发布模式下编译代码

在预处理器定义中设置_SECURE_SCL = 0.

此外,boost :: progress_timer的分辨率为毫秒,我相信(它基于std :: clock),这使得它对于短持续时间的精确测量非常不可靠。您需要使测量的代码显着变慢,以便摆脱其他因素(例如您的进程处于保持状态等)。您应该使用DeadMG建议的高性能计数器进行测量。

答案 2 :(得分:2)

find不接受value_type,它需要一个const value_type&amp;。现在,我会说对于unsigned int,这应该没有区别。但是,您的优化器完全可能没有注意到这一点,并且无法正确优化循环体。

编辑:我建议你使用for循环对编译器说谎。您可以将其重写为

typename Vec::iterator i, end;
i = vec.begin();
end = vec.end();
while(i != end && *i != val)
    i++;
return i;

当然,编写std :: find的人确切地知道优化器有多聪明,以及它究竟能够和不能应付什么。

编辑:我在我的机器上运行了测试。这是在Visual Studio 2010上的i7 930,没有超频。我用高性能计数器取代了boost :: progress_timer。

__int64 frequency, begin, end;
QueryPerformanceCounter(frequency);
double d;
QueryPerformanceCounter(begin);
iter found=find1(vec, vec.capacity());
QueryPerformanceCounter(end);
d = ((end - begin) / (double)frequency) * 1000000;
cout << "first search required: " << d << endl;
cout << "first search found value: " << value_found(vec, found)<< endl;


QueryPerformanceCounter(begin);
found=find2(vec, vec.capacity());
QueryPerformanceCounter(end);
d = ((end - begin) / (double)frequency) * 1000000;
cout << "second search required: " << d << endl;
cout << "second search found value: " << value_found(vec, found)<< endl;

说它们都需要0.24(大约)纳秒来操作 - 也就是说,没有区别。我的建议是你的优化器只是不成熟,而你的std :: find版本正是为了呈现正确的优化而编写的,而你的find只是没有勾选正确的优化框。

编辑:您的计时数据明显被破坏。我的i7运行在0.23纳秒 - 即0.00000023秒,而你的需要0.008秒。除非我的i7比你的快了大约40,000倍,否则就没办法了。 i7无法用这么长的时间来遍历千万件物品。当然,我实际上运行64位Windows 7,虽然没有在64位模式下编译。

现在要发布反汇编程序。

FIND1:

00F810D3  mov         esi,dword ptr [esp+34h]  
00F810D7  mov         eax,dword ptr [esp+3Ch]  
00F810DB  mov         ecx,dword ptr [esp+38h]  
00F810DF  sub         eax,esi  
00F810E1  sar         eax,2  
00F810E4  cmp         esi,ecx  
00F810E6  je          main+0B3h (0F810F3h)  
00F810E8  cmp         dword ptr [esi],eax  
00F810EA  je          main+0B3h (0F810F3h)  
00F810EC  add         esi,4  
00F810EF  cmp         esi,ecx  
00F810F1  jne         main+0A8h (0F810E8h)  

find2:

00F8119A  mov         ecx,dword ptr [esp+34h]  
00F8119E  mov         eax,dword ptr [esp+3Ch]  
00F811A2  mov         edx,dword ptr [esp+38h]  
00F811A6  sub         eax,ecx  
00F811A8  sar         eax,2  
00F811AB  cmp         ecx,edx  
00F811AD  jae         main+17Fh (0F811BFh)  
00F811AF  nop  
00F811B0  cmp         dword ptr [ecx],eax  
00F811B2  je          main+254h (0F81294h)  
00F811B8  add         ecx,4  
00F811BB  cmp         ecx,edx  
00F811BD  jb          main+170h (0F811B0h)  
00F811BF  mov         esi,edx  

您可以看到find2 与find1略有不同。我通过将另一个find1调用替换为find2的调用来检查, 生成相同的反汇编。很奇怪他们会产生不同的装配。

答案 3 :(得分:1)

您的测量方法已被破坏。测量代码的执行速度非常难以正确执行,因为总耗用时间取决于您编写的代码可能无法明确控制的因素。

要检查的一些事情(你可能会考虑其中一些显而易见的事):

  • 您使用的是哪些优化设置?您正在测试发布版本,对吗?
  • 你说你已经检查了STL版本生成的汇编代码,但它没有使用矢量化。但也许它正在使用其他一些常见的优化方法,如循环展开?
  • 为什么在循环中使用i < end而不是i != end? (我实际上怀疑这有什么不同,但谁知道呢?)

(我原来的答案完全是愚蠢的 - 不知道为什么会投票 - 我将其留在这里,因为有些评论与此有关)

在这种情况下,我怀疑你只是看到了内存层次结构的影响。当你调用find1()时,CPU必须从RAM中读取所有数据。然后,该数据将存储在CPU的缓存中,这比访问RAM要快很多(容易10到100倍)。当你调用find2()时,CPU可以从高速缓存中读取整个数组,因此find2()的执行时间更短。

要获得更多证据,请尝试交换代码,以便先测量find2()然后再测量find1()。如果结果反转,则可能是您看到了缓存的影响。如果他们不这样做,那就是别的了。

修改 经过一番深思熟虑(实际上,就在一些之后),我认为我原来的怀疑一定是不正确的(你正在搜索的数组的大小使整个数组不太可能被放入缓存)。可能仍然存在缓存效果,但它们可能更微妙。不过,无论如何都要尝试颠倒测量,看看它有什么影响会很有趣。

答案 4 :(得分:0)

我自己不使用Visual C ++,但是对于GCC我也得到find2稍微慢一点的结果。但是,通过手动展开循环,我能够使find2略快于find1

template<class Vec>
typename Vec::const_iterator find2(Vec const& v, typename Vec::value_type val)
{
  for(typename Vec::const_iterator i=v.begin(), end=v.end(); i != end; ) {
    if (i[0]==val) return i;
    if (i[1]==val) return i + 1;
    i += 2;
  }
  return v.end();
}

我对std::find更快的猜测是编译器拥有所有信息来确定向量大小是2的倍数,并且可以进行展开。

另一个猜测是它只是在空间/大小之间进行权衡 - 编译器在一般情况下会跳过此优化。

答案 5 :(得分:-1)

许多C / C ++用户抱怨说,一旦他们编写了函数的特化,非专业版就会执行它!

原因很简单,一旦你在编译器后端编写优化传递,你就会想到改进std::find代码生成的方法,因此它会执行你的实现。

此外,至少VC ++ std::find的节点具有不同的版本,它将为不同类型的迭代器调用不同的函数和搜索算法。

所以我觉得编译器似乎都理解你的数据是排序的,因此可以更好地搜索。