众核CPU:避免令人失望的可扩展性的编程技术



我们刚刚买了一台32核Opteron机器,我们得到的加速有点令人失望:超过大约24个线程后,我们看不到任何加速(实际上总体上变慢了),大约6个线程后它变得明显次线性。

我们的应用程序对线程非常友好:我们的工作分为大约170000个小任务,每个任务都可以单独执行,每个任务需要5-10秒。它们都从相同的内存映射文件中读取,大小约为4Gb。他们偶尔会对它进行写入,但每次写入可能需要10000次读取——我们只是在170000个任务中的每一个任务结束时写入一点数据。写操作受锁定保护。分析显示锁不是问题。线程在非共享对象中使用了大量JVM内存,它们对共享JVM对象的访问非常少,其中只有一小部分访问涉及写入。

我们在Linux上用Java编程,并启用了NUMA。我们有128Gb的RAM。我们有2个Opteron CPU(型号6274),每个CPU有16个核心。每个CPU有2个NUMA节点。在Intel四核(即8核)上运行的相同作业几乎线性地扩展到8个线程。

我们已经尝试复制只读数据,使每个线程有一个,希望大多数查找都可以是NUMA节点的本地查找,但我们没有观察到任何加速。

对于32个线程,"top"显示CPU 74%的"us"(用户)和约23%的"id"(空闲)。但这里没有睡眠,几乎没有磁盘i/o。使用24个线程,我们可以获得83%的CPU使用率。我不知道如何解释"空闲"状态——这是否意味着"等待内存控制器"?

我们试着打开和关闭NUMA(我指的是需要重新启动的Linux级别设置),但没有发现任何区别。启用NUMA时,"numastat"仅显示约5%的"分配和访问未命中"(95%的缓存未命中是NUMA节点的本地缓存)。[编辑:]但是添加"-XX:+useNUMA"作为java命令行标志给了我们10%的提升。

我们的一个理论是,我们正在最大限度地使用内存控制器,因为我们的应用程序使用了大量RAM,并且我们认为存在大量缓存未命中。

我们能做些什么来(a)加快我们的程序以接近线性可伸缩性,或者(b)诊断发生了什么

还有:(c)我如何解释"top"结果——"idle"是否意味着"在内存控制器上被阻塞"?(d)Opteron和Xeon的特性有什么不同吗?

我还有一台32核Opteron机器,有8个NUMA节点(4x6128处理器,Mangy Cours,而不是推土机),我也遇到过类似的问题。

我认为顶部显示的2.3%的"系统"时间暗示了你问题的答案。根据我的经验,这个系统时间是系统在内核中等待锁的时间。当线程无法获得锁时,它将处于空闲状态,直到它进行下一次尝试。系统时间和空闲时间都是锁争用的直接结果。你说你的探查器没有显示锁是问题所在。我的猜测是,由于某种原因,导致锁定的代码没有包含在配置文件结果中。

在我的案例中,锁争用的一个重要原因不是我实际正在做的处理,而是将单个工作分配给每个线程的工作调度器。这段代码使用锁来跟踪哪个线程在做哪项工作。我对这个问题的解决方案是重写我的工作调度程序,避免互斥,我读到互斥不能很好地扩展到8-12个核心,而是使用gcc内置原子(我在Linux上用C编程)。原子操作实际上是一个非常细粒度的锁,在高内核数的情况下可以更好地扩展。在你的情况下,如果你的工作包真的需要5-10秒,这对你来说似乎不太可能意义重大。

我也遇到了malloc的问题,它在高内核数的情况下会遇到可怕的锁定问题,但我一时想不起来这是否也导致了sys&顶部的空闲数字,或者它是否只是使用Mike Dunlavey的调试器评测方法显示的(我如何评测在Linux中运行的C++代码?)。我怀疑它确实导致了系统和;空闲的问题,但我在挖掘所有旧笔记时划清界限:)我知道我现在尽可能避免运行时mallocs。

我的最佳猜测是,您正在使用的某些库代码在您不知情的情况下实现了锁,没有包含在您的分析结果中,并且不能很好地扩展到高内核数的情况。小心内存分配器!

我相信答案将取决于对硬件架构的考虑。你必须把多核计算机看作是通过网络连接的单个机器。事实上,这就是Hypertransport和QPI的全部功能。

我发现,要解决这些可伸缩性问题,你必须停止从共享内存的角度思考,开始采用通信顺序过程的哲学。这意味着要以完全不同的方式思考(想象一下,如果你的硬件是由网络连接的32台单核机器,你会如何编写软件)。现代(和古代)CPU架构的设计并不是为了提供你所追求的那种不受限制的缩放。它们的设计允许许多不同的进程继续处理自己的数据。

就像计算中的其他东西一样,这些东西也很时髦。CSP可以追溯到20世纪70年代,但非常现代的Java衍生Scala是这一概念的流行体现。请参阅维基百科上关于Scala并发性的部分。

CSP的理念是迫使你设计一个适合你的数据和你正在解决的问题的数据分发方案。这不一定很容易,但如果你能管理好它,那么你就有了一个可以很好扩展的解决方案。Scala可能会让开发变得更容易。

就我个人而言,我在CSP和C中做每件事。这使我能够开发一个信号处理应用程序,该应用程序可以从8个核到几千个核(限制是我的房间有多大)完美地线性扩展。

你要做的第一件事就是实际使用NUMA。这不是一个你打开的神奇设置,你必须在你的软件架构中利用它。我不知道Java,但在C中,人们会将内存分配绑定到特定核心的内存控制器(也称为内存关联),在操作系统没有得到提示的情况下,线程也会如此(核心关联)。

我想你的数据不会分解成32个整洁、离散的块吗?在不确切了解程序中隐含的数据流的情况下,很难给出建议。但从数据流的角度来考虑。均匀地画出来;数据流图对此很有用(另一种古老的图形形式表示法)。如果你的图片显示你的所有数据都通过一个对象(例如通过一个内存缓冲区),那么它会很慢。。。

我假设您已经优化了您的锁,并且同步化达到了最低限度。在这种情况下,它仍然在很大程度上取决于您使用什么库来进行并行编程。

即使没有同步问题,也可能发生一个问题,即内存总线拥塞。这是非常恶劣和难以摆脱的。我所能建议的就是以某种方式让你的任务变得更大,创造更少的任务。这在很大程度上取决于你问题的性质。理想情况下,您需要与核心/线程数量一样多的任务,但这并不容易(如果可能的话)实现。

另外一个可以提供帮助的方法是为JVM提供更多堆。这将减少频繁运行垃圾回收器的需要,并稍微加快速度。

"空闲"是否意味着"在内存控制器上被阻止">

否。你在上面看不到。我的意思是,如果CPU正在等待内存访问,它将显示为繁忙。如果你有空闲时间,它要么在等待锁定,要么在等待IO.

我是原创海报。我们认为我们已经诊断出了问题,这不是锁,不是系统调用,也不是内存总线拥塞;我们认为这是级别2/3的CPU缓存争用

重申一下,我们的任务令人尴尬地平行,因此应该规模很大。然而,一个线程有大量的CPU缓存可以访问,但随着我们添加更多的线程,每个进程可以访问的CPU缓存数量越来越少(相同数量的缓存除以更多的进程)。一些架构的某些级别在裸片上的核心之间共享,有些甚至在裸片之间共享(我认为),这可能有助于对您正在使用的特定机器进行"深入研究",并优化您的算法,但我们的结论是,要实现我们认为的可扩展性,我们无能为力。

我们通过使用两种不同的算法来确定这是原因。访问更多2/3级缓存的缓存比用更少数据进行更多处理的缓存扩展得更差。它们都频繁地访问主存储器中的主数据。

如果您还没有尝试过:看看像Oracle Studio这样的硬件级评测器(适用于CentOS、Redhat和Oracle Linux),或者如果您一直使用Windows:Intel VTune。然后开始研究每个指令的时钟指标高得令人怀疑的操作。可疑的高意味着比单个numa、单个L3-缓存机器上的相同代码高得多(就像当前的英特尔台式CPU一样)。

最新更新