【问题标题】:optimizing `std::vector operator []` (vector access) when it becomes a bottleneck当它成为瓶颈时优化`std::vector operator []`(向量访问)
【发布时间】:2011-11-26 07:45:55
【问题描述】:

gprof 说我的高计算应用程序将 53% 的时间花在 std::vector <...> operator [] (unsigned long) 内,其中 32% 用于一个使用频繁的向量。更糟糕的是,我怀疑我的并行代码无法扩展到超过 3-6 个内核是由于相关的内存瓶颈。虽然我的应用程序确实花费了大量时间访问和写入内存,但似乎我应该能够(或至少尝试)做得好于 52%。我应该尝试使用动态数组(在大多数情况下大小保持不变)吗?这可能有助于解决可能的瓶颈吗?

实际上,我首选的解决方案是解决瓶颈问题,并为方便起见保留向量。基于以上,是否有任何可能的罪魁祸首或解决方案(tcmalloc 已出)?

【问题讨论】:

  • 嗯,它需要百万分之一纳秒的 53% 吗?如果是这样,它确实做得很好。我不认为占用 53% 的时间本身就是一件坏事,除非那个时间非常长。该计划总体需要多长时间,与什么相关?
  • 嗯,std::vector 基本上 一个动态数组,所以改变它不会有任何好处。您是否真的在启用优化/发布模式的情况下进行编译?
  • AFAIK,gprof 无法分析内联代码。 [] 应该被内联,如果它出现,我的猜测是它不是。
  • @Matt:使用-O 标志启用优化。默认为-O0(无优化)。试试-O2(或更高版本)。
  • 两件事:第一:用最大优化编译;否则询问性能完全没有意义。第二:确保你的并发确实有意义。是否有很多锁定?

标签: c++ multithreading vector stl


【解决方案1】:

您是否检查过您的内存访问模式本身?它可能效率低下 - 缓存不友好。

【讨论】:

  • @Matt Munson:很好的问题,实际上。我不知道有什么工具可以帮助你做到这一点,所以我只能建议分析算法。
  • 我会寻找什么?缓存不友好算法的特点是什么?
  • @Matt Munson:内存喜欢顺序前向访问。当您读取内存地址 A 时,会预先缓存一些其他地址。此外,在这种情况下,内存以突发模式访问,传输的不是单个元素,而是块。如果您尝试向后读取内存,这一切都会改变,甚至是顺序方式。没有什么比完全随机访问大内存块更糟糕的了。顺便说一句,您的向量的元素数量是多少?
  • 快速总结起来可能有点困难。这条线是我的应用程序的主要主力:add_vector <compute_sign>( (*targ)[w], (source[q] * (syns[i].weight + syns[i].rmod)) ); 它的内部 3 个循环,其中两个取决于 if 语句。矢量syns[] 的长度为 72、36、8 或 3。你愿意在聊天中看看我的代码吗?
  • @MattMunson 最明显的问题是迭代是如何执行的(避免跳得太快)以及如何在不同进程之间分配工作负载(避免错误共享)接下来的事情是是否您可以缓存一些中间结果。这些在您提供的信息中都看不到。
【解决方案2】:

您是否尝试在访问数组时使用原始指针?

// regular place

for (int i = 0; i < arr.size(); ++i)
    wcout << arr[i];

// In bottleneck

int *pArr = &arr.front();

for (int i = 0; i < arr.size(); ++i)
    wcout << pArr[i];

【讨论】:

  • 这将完全一样,因为vector&lt;T&gt;::operator[] 只是转发到它的内部动态数组。
  • @Xeo 是的,但转发可能需要相当多的时间。也尝试这样做可能会排除仅使用 OP 正在考虑做的动态数组的优化。
  • @SethCarnegie:附议。我已经看到 -O0 和 -O2 之间的数量级加速,仅仅是因为迭代器转发被内联了。
  • @mosh:它内部不使用迭代器,只使用它的原始指针。基本上是return *(ptr + pos);
  • @Xeo:优化到位后仍然有巨大的加速。快速指针增量和更好的内联是两个很好的理由。与分析发布版本相比,分析调试版本有点毫无意义。
【解决方案3】:

我怀疑 gprof 会阻止函数被内联。尝试使用另一种分析方法。 std::vector operator [] 不能成为瓶颈,因为它与原始数组访问没有太大区别。 SGI 实现如下图所示:

reference operator[](size_type __n) { return *(begin() + __n); }
iterator begin() { return _M_start; }

【讨论】:

  • gprof 不会阻止任何事情 - 使用调试符号进行编译并且没有优化是阻止内联的原因。
  • @Pubby:要获得调用图和准确调用计数等信息,您还需要检测函数。这将对快速执行的代码产生巨大的影响(基本上使您可以从 gprof 中获得的任何数字完全没有意义)。
【解决方案4】:

你不能相信gprof 进行高速代码分析,你应该使用像oprofile 这样的被动分析方法来获得真实的图片。

作为替代方案,您可以通过手动代码更改来分析(例如,调用计算 10 次而不是一次并检查执行时间增加了多少)。请注意,这将受到缓存问题的影响,因此 YMMV。

【讨论】:

  • 我尝试了oprofileperf,不幸的是它们在我的目标平台虚拟化(EC2 HPC)机器上不可行。我不认为有一种方便的方法吗?
【解决方案5】:

vector 类很受欢迎,它提供了一定程度的便利,但牺牲了性能,这在你不是特别需要性能的情况下很好。

如果你真的需要性能,绕过向量类直接进入一个简单的老式手工数组,无论是静态分配还是动态分配,都不会对你造成太大伤害。然后 1)您当前花费在索引上的时间应该基本上消失,从而使您的应用程序加速该数量,并且 2)您可以继续处理您的应用程序中需要时间的“下一件大事”。

编辑: 大多数程序的加速空间比你想象的要大得多。我做了一个walk-through project 来说明这一点。如果我能很快总结出来,它是这样的:

  • 每个“作业”的原始时间是 2.7 毫秒(“作业”的数量可以变化以获得足够的运行时间来分析它)。

  • 第一次剪辑显示大约 60% 的时间用于向量操作,包括索引、追加和删除。我用 MFC 中的类似向量类替换,时间减少到 1.8 毫秒/作业。 (这是 1.5 倍或 50% 的加速。)

  • 即使使用该数组类,也有大约 40% 的时间花在 [] 索引运算符上。我希望它直接索引,所以我强制它直接索引,而不是通过操作符函数。这将时间减少到 1.5 毫秒/作业,速度提高了 1.2 倍。

  • 现在大约 60% 的时间是在数组中添加/删除项目。额外的一部分用于“新建”和“删除”。我决定放弃阵列并做两件事。一种是使用自己动手做的链表,并汇集使用过的对象。第一次将时间减少到 1.3 毫秒 (1.15x)。第二个将其减少到 0.44 毫秒 (2.95x)。

  • 在那段时间里,我发现大约 60% 的时间是在我编写的用于对列表进行索引的代码中(就像它是一个数组一样)。我决定可以通过将指针直接指向列表来完成。结果:0.14 毫秒 (3.14x)。

  • 现在我发现几乎所有时间都花在了我打印到控制台的诊断 I/O 行上。我决定摆脱它:0.0037 毫秒 (38x)。

我本可以继续前进,但我停了下来。 每个作业的总时间减少了大约 700 倍。

我想让你明白的是,如果你需要足够糟糕的性能来偏离可能被认为是公认的做事方式,你不必在遇到一个“瓶颈”后停下来。 仅仅因为你得到了很大的加速并不意味着没有更多。 事实上,就加速因素而言,下一个“瓶颈”可能比第一个更大。 因此,提高您对可以获得的加速的期望值,然后全力以赴。

【讨论】:

  • 在启用优化的情况下使用 std::vector&lt;&gt; 时,您认为性能开销是多少?我保证没有没有。禁用优化的分析代码简直是愚蠢的。
  • @ildjarn:您希望启用优化会导致 [] 被内联。这样做的问题是很难进行进一步的采样。另一方面,您可以强制它直接索引,而不会失去进行进一步采样的能力。如果您失去进行诊断采样的能力,加速的进程就会停止。另外,请注意,虽然直接索引节省了时间,但完全摆脱向量可以节省更多时间,在这个程序中
  • 听起来您在指出各种分析方法的缺点,而不是解释为什么std::vector&lt;&gt; 比启用优化的普通 C 数组慢的任何原因。
  • @ildjarn:是的。假设有一场比赛。谁能编写程序以在特定的机器上以最少的时间完成特定的工作,没有什么比这更重要了?我更喜欢的方法是在最后打开优化。在我尽可能挤出每个循环之后,让编译器完成它的工作。
  • @ildjarn:看,在程序的初始版本中,我使用了向量,因为,为什么不呢?他们工作。但事实证明,经过一些调整,它们是“瓶颈”,链表可以节省时间。这只是证实了不同的数据结构在不同的情况下更好。重要的是您如何找出要修复的内容,并且连续修复会产生复合加速因素。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-06-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多