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

问题描述 投票:7回答:5

我们刚刚购买了一台32核的Opteron机器,我们获得的加速有点令人失望:超过大约24个线程我们看不到加速(实际上总体上变慢)并且在大约6个线程之后它变得非常线性。

我们的应用程序非常适合线程:我们的工作分为大约170,000个小任务,每个任务可以单独执行,每个任务需要5-10秒。它们都是从大小约为4Gb的相同内存映射文件中读取的。它们偶尔写入它,但每次写入可能有10,000次读取 - 我们只是在170,000个任务的每一个末尾写入一些数据。写入受锁保护。分析表明锁不是问题。线程在非共享对象中使用大量JVM内存,并且它们对共享JVM对象的访问非常少,而且只有一小部分访问涉及写入。

我们在Linux上使用NUMA进行Java编程。我们有128Gb RAM。我们有2个Opteron CPU(型号6274),每个16核。每个CPU有2个NUMA节点。在英特尔四核(即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)我如何解释“顶部”结果 - “空闲”是否意味着“在内存控制器上被阻止”? (d)Opteron与Xeon的特性有何不同?

parallel-processing cpu multicore numa
5个回答
3
投票

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

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

在我的情况下,锁争用的一个重要原因不是我实际正在进行的处理,而是工作调度程序将各个工作分发给每个线程。这段代码使用锁来跟踪哪个线程正在做哪个工作。我解决这个问题的方法是重写我的工作调度程序,避免使用互斥体,我读过这些互斥体的规模远不超过8-12个核心,而是使用gcc builtin atomics(我在Linux上使用C编程)。原子操作实际上是一种非常精细的锁定,可以在高核心数的情况下更好地扩展。在您的情况下,如果您的工作包确实每个需要5-10个,这对您来说似乎不太重要。

我也遇到了malloc的问题,它在高核心数的情况下遭遇了可怕的锁定问题,但我不能,在我的头脑中,记住这是否也导致系统和空闲数字在顶部,或者它是否刚刚出现使用Mike Dunlavey的调试器分析方法(How can I profile C++ code running in Linux?)。我怀疑它确实导致系统和空闲问题,但我在挖掘所有旧笔记时划清界线以找出:)我知道我现在尽可能避免运行时mallocs。

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


1
投票

我假设您已经优化了锁定,并且同步最小化。在这种情况下,它仍然取决于您使用哪些库并行编程。

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

其他可以帮助的是为您的JVM提供更多堆。这将减少频繁运行垃圾收集器的需求,并加快一点。

“空闲”意味着“内存控制器被阻止”

不,你没有看到它在顶部。我的意思是如果CPU正在等待内存访问,它将显示为忙碌。如果您有空闲时段,则要么等待锁定,要么等待IO。


1
投票

我确信答案将在于考虑硬件架构。您必须将多核计算机视为通过网络连接的单个计算机。事实上,这就是Hypertransport和QPI的全部内容。

我发现要解决这些可扩展性问题,你必须停止考虑共享内存并开始采用通信顺序进程的理念。这意味着思维方式不同,即想象如果您的硬件是由网络连接的32台单核机器,您将如何编写软件。现代(和古老)CPU架构的设计并不是为了实现您所追求的类型的自由缩放。它们旨在允许许多不同的进程继续处理自己的数据。

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

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

我个人在CSP和C中做了一切。它允许我开发一个信号处理应用程序,可以完全线性地从8个核心扩展到几千个核心(限制是我的房间有多大)。

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

我认为你的数据不会分解成32个整齐,分散的块?如果不确切知道程序中隐含的数据流,就很难提供建议。但从数据流方面考虑一下。把它画出来;数据流图对此有用(另一种古老的图形形式表示法)。如果你的图片显示所有数据通过单个对象(例如通过单个内存缓冲区),那么它将会很慢......


0
投票

如果你还没有尝试过:看看像Oracle Studio这样的硬件级分析器(对于CentOS,Redhat和Oracle Linux),或者如果你遇到Windows:Intel VTune。然后开始查看每个指令指标具有可疑高时钟的操作。可疑的高意味着比单核,单L3缓存机器(如当前的英特尔台式机CPU)上的相同代码高很多。


0
投票

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

重申一下,我们的任务是令人尴尬的平行,所以它应该很好地扩展。但是,一个线程可以访问大量的CPU缓存,但是当我们添加更多线程时,每个进程可以访问的CPU缓存量越来越低(相同数量的缓存除以更多进程)。某些架构上的某些级别在芯片上的内核之间共享,有些甚至在芯片之间共享(我认为),它可能有助于使用您正在使用的特定机器“降低杂草”,并优化您的算法,但我们的结论是,我们无法实现我们认为可获得的可扩展性。

我们通过使用2种不同的算法将其确定为原因。访问更多级别2/3缓存的那个比使用更少数据进行更多处理的缓存更糟糕。它们都经常访问主存储器中的主数据。

© www.soinside.com 2019 - 2024. All rights reserved.