【问题标题】:STL find performs better than hand-crafted loopSTL find 比手工制作的循环表现更好
【发布时间】:2011-06-02 15:11:07
【问题描述】:

我有一些问题。给定以下 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 find(通过 find1 调用)的运行速度比手工循环(通过 find2 调用)快大约 10 倍。我首先认为 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

非常感谢,

杜莎。

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

附言我被要求发布为查找功能生成的程序集。这里是:

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 版本中,没有推测对齐方式。我说的对吗?

【问题讨论】:

  • 快多少?你能发布你的程序输出吗?
  • 在每一种实验中,单个数据点都没有任何意义。我建议您循环重复每次搜索,测量每次迭代的时间,最后获得测量时间的平均值和标准偏差。平均值应该消除大部分测量噪声(“加热”缓存和让随机波动自我补偿),stddev 应该让您衡量这种波动有多大,以及您的结果有多可靠。如果执行此操作后,时间仍然相差超过 2σ,您可能应该使用分析器仔细查看。
  • 按照您的建议进行...结果是:需要第一次搜索:0.01004,需要第二次搜索:0.02006。只需运行循环 100 次并计算平均值。
  • 这可能不是一个好主意:i&lt;end 使用i != end
  • find1() 分配给在构造过程中找到的。 find2() 使用复制构造分配给 found 。这可能没什么大不了的,但是在计时时,您应该始终在任何地方做同样的事情(除了您正在计时的事情)。

标签: c++ stl assembly find performance


【解决方案1】:

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

原因很简单,一旦您在编译器后端编写优化传递,您就会想办法改进std::find 代码生成,从而使其执行您的实现。

至少对于 VC++ 来说,std::find 的节点也有不同的版本,它们会针对不同类型的迭代器调用不同的函数和搜索算法。

所以我认为编译器似乎理解您的数据已排序,因此执行更好的搜索。

【讨论】:

  • “所以我认为编译器似乎理解你的数据是排序的......”
  • 我几乎不怀疑,只是把 random_shuffle(vec.begin(), vec.end());在生成调用之后。现在差异是相同的(乘以 10),但整体时间更短。我只能解释一下,那部分数据还在缓存中。
  • 实际上前 2 行可能是真的:我确实使用这种技术来改进我的优化器。
  • 有可能。即使编译器无法猜测数据,从汇编中可以明显看出它可以更好地猜测数据结构,从而生成更好的代码。
【解决方案2】:

您的衡量方法有问题。正确测量代码的执行速度非常困难,因为总运行时间取决于您编写的代码可能未明确控制的因素。

需要检查的一些事项(您可能会认为其中一些显而易见):

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

(我最初的答案完全是愚蠢的——不知道为什么它得到了投票——我把它留在这里,因为一些 cmets 与它有关)

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

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

编辑 经过进一步思考(实际上,只是在 some 思考之后),我认为我最初的怀疑一定是不正确的(您正在搜索的数组的大小使得整个数组不太可能被放入缓存)。可能仍然存在缓存效果,但它们可能更加微妙。不过,无论如何尝试反转测量值,看看它有什么效果会很有趣。

【讨论】:

  • 向量为 sizeof(unsigned int)*10 000 000 字节。是不是太大了不适合cpu缓存? OP还说 find1 (第一次调用)更快。所以你的解释似乎有点不对劲。
  • 是的。我的建议是虚假的,原因有几个(数据太大,当然它也会在生成后立即在缓存中)。缓存可能仍然在速度差异中发挥作用,但不是一个完全简单的部分。
  • find2 需要更多时间来执行!即使我交换执行结果也是一样的。
  • 检查了你的观点......唯一的事情:我将 '
  • 编译器可能不会展开你的循环,因为现代编译器不会展开任何东西,因为展开通常会减慢现代处理器的速度。由于缓存访问减少、分支预测更容易、指令管道填充更容易等,超紧密循环通常比展开循环更快。它越小,各级缓存越好,因此速度更快。这不是普遍正确的,但对于编译器来说是最好的启发式方法。
【解决方案3】:

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

在您的预处理器定义中设置 _SECURE_SCL=0。

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

【讨论】:

  • 我在发布模式下编译它。做了,没区别。已检查的迭代器用于 STL 和我的循环中,因为它是一个迭代器属性...
  • @dusha:你把#define放在哪里了?它必须在实际包含 find 之前发生。
  • @dusha:发布模式还不够。您需要明确禁用检查的迭代器。在程序顶部将_SECURE_SCL定义为0,使得手写循环与stl::find一样快。
【解决方案4】:

find 不需要 value_type,它需要一个 const value_type&。现在,我想说对于 unsigned int,这应该没有什么区别。但是,您的优化器很可能只是没有注意到这一点,并且未能正确优化您的循环体。

编辑:我的建议是,您使用 for 循环有点对编译器撒谎。您可以将其重写为

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

当然,编写 std::find 的人确切地知道优化器有多聪明,以及它到底能处理什么,不能处理什么。

编辑:我在我的机器上运行了你的测试。这是一个 i7 930,没有超频,在 Visual Studio 2010 上。我用高性能计数器替换了 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 秒,而你的 i7 需要 0.008 秒。除非我的 i7 比你的快 40,000 倍,否则没有办法。 i7 不可能只遍历 1000 万个项目需要这么长时间。当然,我实际上运行的是 64 位 Windows 7,虽然没有在 64 位模式下编译。

现在要发布反汇编程序。

找到1:

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)  

查找2:

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 略有不同。我通过将 find2 调用替换为另一个 find1 调用来检查,确实会产生相同的反汇编。好奇他们生产不同的程序集。

【讨论】:

  • 我在 32 位机器上做... sizeof(reference)==sizeof(unsigned int)。为什么这很重要?并且 find 只调用一次,因此它不会只复制一次值并使用引用运行循环。
  • 您发布的代码不是我的发现的实现...只是测试了一下。它比 find 慢。实际上,我手工制作的循环提供了与您的 sn-p 相同的性能。
  • @dusha:如果优化器无法发现它们相同,这很重要。
  • +1 用于指向高性能计数器。 boost::progress_timer 不适用于这样的性能基准测试 - 它基于 std::clock。
  • 你没有发布反汇编程序。
【解决方案5】:

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

我的另一个猜测是,您在find2 中的循环的每次迭代中都调用std::vector&lt;t&gt;::end(),而std::find 只会调用一次开始和结束访问器。 我是个白痴。

【讨论】:

  • 如果你看一下代码,你会发现 end() 并不是每次迭代都被调用。
  • @dusha:但是find 可以通过仅在进入算法时进行检查来分摊检查成本——即使算法仍然处于检查状态,它也会在内部使用未经检查的操作。
  • 检查/未检查迭代器问题是关键。如果在程序开头添加#define _SCL_SECURE 0,find2 会变得和 find1 一​​样快。
  • @Adrian:你可能还得玩_SCL_HAS_ITERATOR_DEBUGGING——不确定。
【解决方案6】:

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

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 的倍数,并且可以执行此展开。

另一个猜测是,这只是空间/大小之间的权衡——编译器在一般情况下会跳过这种优化。

【讨论】:

  • 非常有趣。仅供参考,您可以发布处理器、g++ 版本和编译标志吗?
  • 我真的很想知道为什么 VC 或 G++ 不展开手工制作的循环?这是他们优化的一部分...经过测试...我的手工展开循环仍然较慢...您的代码有一个小错误...返回必须返回 i+OffsetValue 而不仅仅是 i。跨度>
  • @dusha:注意到并修复了。
  • @dusha:我再次检查了 - 我的时间差异比你的小得多 - 所以这可能不是你问题的解释。
猜你喜欢
  • 1970-01-01
  • 2013-06-28
  • 2019-02-08
  • 1970-01-01
  • 2014-02-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-01-03
相关资源
最近更新 更多