并行代码可扩展性差

最近我一直在分析我的并行计算如何实际加速16核处理器。 我得出的一般公式 – 你得到的每个核心速度越慢的线程就越让我感到尴尬。 以下是我的CPU负载和处理速度的图表:

图片1

因此,您可以看到处理器负载增加,但速度增加得慢得多。 我想知道为什么会发生这样的影响,以及如何得到不可伸缩行为的原因。 我已确保使用服务器GC模式 。 一旦代码不做任何事情,我就确保我正在并行化适当的代码

  • 从RAM加载数据(服务器有96 GB的RAM,不应该触发交​​换文件)
  • 执行不复杂的计算
  • 在RAM中存储数据

我仔细分析了我的应用程序并发现没有瓶颈 – 看起来每个操作随着线程数的增长而变慢。

我被困了,我的情景出了什么问题?

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

线性可扩展性的关键 – 在从一到两个内核的吞吐量增加一倍的情况下 – 尽可能少地使用共享资源。 这意味着:

  • 不要使用超线程(因为两个线程共享相同的核心资源)
  • 将每个线程绑定到特定的核心(否则操作系统将在核心之间兼顾线程)
  • 不要使用比核心更多的线程(操作系统将交换进出)
  • 留在核心自己的缓存中 – 现在是L1和L2缓存
  • 除非绝对必要,否则不要冒险进入L3缓存或RAM
  • 最小化/节省关键部分/同步使用

如果你已经到了这么远,你可能已经对你的代码进行了分析和手动调整。

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

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

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

__ _ __ _ _ 编辑 _ __ _ __ _

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

你将永远得到这种曲线,它被称为Amdahl定律 。
问题是它会在多久后保持平稳。

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

较multithreading的回报率普遍下降可能表明某种瓶颈。

是否有任何共享资源,如集合或队列或某些东西,或者您正在使用某些可能依赖于某些有限资源的外部函数?

8个线程的突然中断很有意思,在我的评论中,我询问CPU是真正的16核还是具有超线程的8核,其中每个核看起来都是OS的2核。

如果它是超线程,那么你要么做得太多,以致超线程不能使核心性能翻倍,要么核心的内存管道无法处理数据吞吐量的两倍。

线程执行的工作是偶数还是某些线程比其他线程更多,这也可能表明资源不足。

由于您添加的线程经常查询数据,这表示存在很大的等待风险。

有没有办法让线程每次都能获得更多数据? 喜欢阅读10个而不是1个?

如果你正在做内存密集型的东西,你可能会达到缓存容量。

您可以使用模拟算法对此进行测试,如果数据一遍又一遍,则只处理相同的小位,因此它们都应该适合缓存。

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

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

这可能会给你带来性能提升,但很可能不会让你达到16倍 – 引擎盖下有很多东西,你只需要逐个淘汰它们。 实际上并不是你的算法在multithreading时以30%的速度运行; 更多的是你的单线程算法以300%的速度运行,由各种CPU和缓存实现,运行multithreading的时间更加困难。 所以没有什么可以“尴尬”的。 但是,通过一些努力,您也许可以让multithreading版本以接近300%的速度运行。

另外,如果你把超线程核心算作真正的核心,那么它们就不是。 它们只允许线程在被阻塞时非常快速地交换。 但他们永远不会让你以双倍的速度奔跑,除非你的线程在一半时间内被阻挡,在这种情况下,这已经意味着你有机会加速。