【问题标题】:Parallel version of loop not faster than serial version循环的并行版本不比串行版本快
【发布时间】:2011-02-07 20:50:12
【问题描述】:

我正在用 C++ 编写一个程序来执行特定系统的模拟。对于每个时间步,执行的最大部分是由单个循环占用。幸运的是,这是令人尴尬的并行,所以我决定使用 Boost Threads 来并行化它(我在 2 核机器上运行)。由于没有锁定,我希望加速接近串行版本的 2 倍。但是我发现根本没有加速。

我实现了循环的并行版本如下:

  • 唤醒两个线程(它们被屏障阻塞)。
  • 然后每个线程执行以下操作:

    • 以原子方式获取和递增全局计数器。
    • 检索具有该索引的粒子。
    • 对该粒子执行计算,将结果存储在单独的数组中
    • 等待作业完成障碍
  • 主线程等待作业完成屏障。

我使用这种方法是因为它应该提供良好的负载平衡(因为每次计算可能需要不同的时间)。我真的很好奇是什么可能导致这种放缓。我总是读到原子变量很快,但现在我开始怀疑它们是否有性能成本。

如果有人有什么想法或任何提示,我将不胜感激。一个星期以来我一直在抨击它,但分析并没有透露太多。

编辑:问题解决了! 我将详细说明我是如何解决这个问题的。我再次使用 gprof,但这次编译时没有优化标志 (-O3)。立即,分析器表明我在对每个粒子执行计算的函数上花费了令人难以置信的时间:比在串行版本中要多得多。

这个函数是虚函数,可以多态访问。我更改了代码以直接访问它,而不是通过 vtable 和瞧,并行版本产生了将近 2 的加速!串行版本的相同更改几乎没有效果。

我不知道为什么会这样,如果有人知道的话会很感兴趣!

感谢所有的海报。你们都在一定程度上有所帮助,很难接受一个答案。

【问题讨论】:

  • 有趣,您是否尝试过仅计时循环本身?是完全没有改善还是只是令人失望的改善?您是否在任务管理器中检查过您的应用程序实际上确实创建了两个线程?您的应用程序内存密集型是否可能遇到内存瓶颈?你也可以尝试简单的拆分数组,让每个线程处理一半,看看会不会有什么不同。
  • 是的,我只对循环进行了计时和分析,没有别的。有一个令人失望的改进:加速范围从 0.9 到 1.1。任务管理器显示两个 cpu 都很忙。线程本身不分配任何新内存。唯一写入单个数组并且它们写入独立位置并且它们从不从同一个数组读取。当我将数组一分为二时,我得到了相似的性能。

标签: performance multithreading parallel-processing boost-thread atomic-values


【解决方案1】:

对该粒子执行计算,将结果存储在单独的数组中

计算量有多大?

  • 一般来说原子计数器可能会花费数百个时钟周期,并且非常重要 看到你不只是增加计数器。
  • 还可以尝试查看每个线程完成了多少工作 - 它们是否合作良好(即在每个循环中,每个线程都会进行大约一半的粒子)。
  • 尝试将作业细分为更大的块,然后是单个粒子(比如说 100 个粒子等)。
  • 查看在线程之外完成了多少工作。

老实说......看起来你在说什么是一个错误。

【讨论】:

  • 计算可能非常繁重,从几毫秒到接近一秒不等。线程是相互独立的,我检查了它们的执行时间,并且大致相等,所以负载平衡正在发生。我会尝试你的建议,考虑 100 个粒子而不是现在只考虑 1 个
  • >>从几毫秒到接近一秒
【解决方案2】:

profiling has not revealed much

这还不清楚。我有在 HP-UX 上分析多线程应用程序的经验,他们的分析器在那里显示每个函数运行的时间百分比。因此,如果您的函数中有一个或几个争用点,您的应用程序在这些函数上花费的时间就会增加。就我而言,pthread_mutex_unlock() 的数量显着增加。当我更改我的代码时,它变得更快了。

那么您能否在此处发布一个线程和两个/四个线程的相同统计信息。以及每次测试的计算次数。

我还建议您(如果可能的话)在锁定互斥锁的全局函数上设置断点。您可能会发现在您的算法中的某个地方偶然锁定了一个全局互斥锁。

【讨论】:

    【解决方案3】:

    你的语言有点暴露:

    等待xxx

    这可能是你的问题。


    另外,再次添加到单个结果队列时会变慢 - 如果可能,您可能仅在处理结束时将结果添加到单个队列中。主线程不要等待,每次更新后都要检查全局计数器。
    我将添加您在最后记录的性能计数器,而不是分析。您可以将它们置于条件编译错误中,以便它们不会添加到您的生产代码中。

    【讨论】:

    • 我也怀疑这可能是问题所在。因此,我用自旋锁替换了屏障,但我仍然得到同样令人失望的表现。在“调试”模式下编译时,我在大多数函数的末尾都有基于时钟()的性能计数器。它们表明两个线程所做的工作量大致相同。
    【解决方案4】:

    你说分析没有透露太多,而且(很遗憾)是典型的。

    我会这样做:

    1. 回到单线程。

    2. 使用this profiling technique that works in any language and environment 使该单线程尽可能快。原因是分析器(大多数但不是全部)只擅长衡量变化,而不是确定你应该修复什么

    3. 然后返回到 1-thread-per-core,并再次执行该过程。如果您发现一个线程或另一个线程在进程间通信上花费了很多时间,那么您需要重新处理它。

    【讨论】:

    • 1) 在过去的几个月里,单线程代码已经成型,如果它可以在不实际改变我正在模拟的模型的动力学的情况下更快地制作,我会感到非常惊讶。 2)我实际上是这种方法的粉丝。我记得前段时间读过你的帖子 :) 3) 遗憾的是根本不应该有进程间通信。这就是奥秘。
    • @Il-Bhima:我仍然很想尝试一下,因为它不花钱,而且它总是能准确地告诉我发生了什么(和没有发生)。它通常很有教育意义。例如,您说不应该有进程间通信。您可以将“应该”替换为“是”或“不是”。
    【解决方案5】:

    我能否建议您发现 OpenMP 更容易实现这种并行性?由于您只想使循环并行,因此您并不想明确地使用线程,而这正是 OMP 真正有效的地方。

    无论如何都值得一试。

    【讨论】:

      猜你喜欢
      • 2016-06-08
      • 1970-01-01
      • 1970-01-01
      • 2022-01-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-12-02
      相关资源
      最近更新 更多