并行代码扩展性差

Posted

技术标签:

【中文标题】并行代码扩展性差【英文标题】:Parallel code bad scalability 【发布时间】:2012-10-02 12:05:48 【问题描述】:

最近我一直在分析我的并行计算如何在 16 核处理器上实际加速。我得出的一般公式——线程越多,每个核心的速度越慢——让我很尴尬。这是我的cpu负载和处理速度的图表:

因此,您可以看到处理器负载增加,但速度增加得慢得多。我想知道为什么会发生这种影响以及如何获得不可扩展行为的原因。 我确保使用Server GC 模式。 我已经确保只要代码执行的只是

,我就会并行化适当的代码 从 RAM 加载数据(服务器有 96 GB 的 RAM,不应该命中交换文件) 执行不复杂的计算 将数据存储在 RAM 中

我已仔细分析我的应用程序,没有发现任何瓶颈 - 看起来每个操作都会随着线程数的增加而变慢。

我卡住了,我的场景有什么问题?

我使用 .Net 4 任务并行库。

【问题讨论】:

它是真正的 16 核 CPU 还是带有超线程的 8 核? 确实如此,但 32 核的速度为 0%,所以我省略了它 线程是预加载数据还是根据需要拉取数据?如果他们拉数据,多久拉一次,是否有等待时间拉? 线程查询数据 - 经常,但我不知道如何衡量和衡量什么 “线程查询数据”——这听起来像是一个热点。 【参考方案1】:

你总会得到这种曲线,它叫做Amdahl's law。 问题是它多久会趋于平稳。

您说您检查了代码是否存在瓶颈,我们假设这是正确的。然后还有内存带宽和其他硬件因素。

【讨论】:

具体有哪些因素以及如何衡量它们?如果可以通过购买新硬件来改进它 - 我会的。 您必须测量/配置文件。例如,很少依赖文件系统和/或 Db。 好吧,我很高兴看到 16 核至少 9 倍。 我认为“你总会得到这种曲线”应该重新表述为“大多数应用程序都会产生这种曲线”。一些应用程序实际上是线性扩展的。 @OlofForshell - 仅当并行部分非常接近 100% 时。作为(非常罕见的)极端案例,该法律实际上涵盖了这一点。在 MiniMax 问题中,甚至有人声称超线性(加速因子 > # 个处理器)【参考方案2】:

线性可扩展性的关键——在从一个内核变为两个内核会使吞吐量翻倍的情况下——是尽可能少地使用共享资源。这意味着:

不要使用超线程(因为两个线程共享相同的核心资源) 将每个线程绑定到一个特定的内核(否则操作系统会处理 内核之间的线程) 使用的线程数不要超过内核数(操作系统将换入和 出) 留在内核自己的缓存中 - 现在是 L1 和 L2 缓存 不要冒险进入 L3 缓存或 RAM,除非它是绝对的 必要的 尽量减少/节省临界区/同步使用量

如果您已经走到这一步,您可能也已经对您的代码进行了概要分析和手动调整。

线程池是一种折衷方案,不适合不折不扣的高性能应用程序。总线程控制是。

不用担心操作系统调度程序。如果您的应用程序受 CPU 限制,需要进行长时间的计算,主要是进行本地 L1 和 L2 内存访问,那么将每个线程绑定到自己的内核是一个更好的性能选择。当然操作系统会进来,但与您的线程执行的工作相比,操作系统的工作可以忽略不计。

另外我应该说我的线程经验主要来自 Windows NT 引擎机器。

_______编辑 _______

并非所有内存访问都与数据读取和写入有关(请参阅上面的注释)。一个经常被忽视的内存访问是获取要执行的代码。所以我关于留在核心自己的缓存中的声明意味着确保所有必要的数据和代码都驻留在这些缓存中。还要记住,即使是非常简单的 OO 代码也可能会生成对库例程的隐藏调用。在这方面(代码生成部门),OO 和解释代码比 C(通常是 WYSIWYG)或者,当然,汇编(完全 WYSIWYG)要少很多所见即所得。

【讨论】:

另外:如果您进行动态内存分配,请使用每线程堆(通过多线程分配器) @KokaChernov 我检查了问题的标签并看到了 C# - 恐怕这不是我的一杯茶。但是如果你有兴趣这里有一个链接 (oracle.com/technetwork/articles/servers-storage-dev/…) 我之前提到过,只有一个线程应该在超线程内核上运行。我最近注意到,如果代码不那么紧凑且优化程度较低,那么超线程在两个这样的代码实例上比一个紧凑的实例可以做更多的事情。所以我猜测超线程以某种方式设法更有效地交织两个代码流:在一个线程中等待内存访问完成时,可能会在另一个线程中执行一条简单的指令。 @trshiv:堆是数据,被“所有必要的数据和代码都驻留在这些缓存中”语句所涵盖【参考方案3】:

更多线程的回报普遍减少可能表明存在某种瓶颈。

是否有任何共享资源,例如集合或队列之类的,或者您是否使用了一些可能依赖于某些有限资源的外部函数?

8 线程的突然中断很有趣,在我的评论中,我询问 CPU 是真正的 16 核还是具有超线程的 8 核,其中每个内核对操作系统显示为 2 个内核。

如果是超线程,要么你的工作量太大,以至于超线程无法将核心性能提高一倍,要么通往核心的内存管道无法处理两倍的数据吞吐量。

线程执行的工作是否均匀,或者某些线程比其他线程做得更多,这也可能表明资源不足。

由于您添加了线程非常频繁地查询数据,这表明等待的风险非常大。

有什么方法可以让线程每次获取更多数据?喜欢阅读 10 条而不是 1 条?

【讨论】:

每个线程做的工作量差不多,所以是平衡的【参考方案4】:

如果您正在执行内存密集型工作,您可能会达到缓存容量。

您可以使用模拟算法进行测试,如果数据一遍又一遍地处理相同的一小部分,那么它应该适合缓存。

如果确实是缓存,可能的解决方案可能是让线程以某种方式处理相同的数据(如小数据窗口的不同部分),或者只是将算法调整为更本地化(如排序,合并排序通常较慢比快速排序,但它对缓存更友好,在某些情况下仍然使它更好)。

【讨论】:

【参考方案5】:

您的线程是否正在读取和写入内存中靠近的项目?那么你可能会遇到虚假分享。如果线程 1 与 data[1] 一起工作,而 thread2 与 data[2] 一起工作,那么即使在理想世界中,我们知道 thread2 两次连续读取 data[2] 将始终产生相同的结果,但在现实世界中,如果 thread1 在这两次读取之间的某个时间更新 data[1],则 CPU 会将缓存标记为脏并更新它。 http://msdn.microsoft.com/en-us/magazine/cc872851.aspx。要解决这个问题,请确保每个线程正在处理的数据在内存中与其他线程正在处理的数据足够远。

这可以提高您的性能,但可能不会让您达到 16 倍 - 引擎盖下发生了很多事情,您只需要一个接一个地淘汰它们。实际上,并不是说您的算法在多线程时以 30% 的速度运行;更重要的是,您的单线程算法以 300% 的速度运行,由运行多线程的各种 CPU 和缓存功能支持,而运行多线程则更难利用。所以没有什么可“尴尬”的。 但是稍加努力,您也可以或许让多线程版本以接近 300% 的速度运行。

此外,如果您将超线程内核视为真正的内核,那么它​​们不是。它们只允许线程在一个被阻塞时快速交换。但是它们永远不会让你以双倍速度运行,除非你的线程有一半的时间被阻塞,在这种情况下,这已经意味着你有机会加速。

【讨论】:

感谢新知识,如果有具体的实现示例会很高兴 @KokaChernov 我没有,只是谷歌“虚假分享”。我知道一种技术是分配大小等于缓存库大小的数组,即使您只使用一个元素。由于数组必须是连续的,这保证了每个线程都在自己的缓存库上工作。虽然它显然浪费了大量的内存。但实际上很多,因为它涉及低级内存管理,在 C++ 中可能会更好,只需 P/Invoke 它。

以上是关于并行代码扩展性差的主要内容,如果未能解决你的问题,请参考以下文章

MapReduce初识

实现并行运算的方法汇总

Apache Spark join 操作扩展性差

Linux AIO:扩展性差

Proteus仿真51单片机+8255并行口扩展流水灯演示

为啥在更多 CPU/内核上的并行化在 Python 中的扩展性如此之差?