除了互斥锁或垃圾回收之外,还有哪些机制会减慢我的多线程 Java 程序的速度?

问题

我有一段java代码(JDK 1.6.0._22,如果相关的话),它实现了一个无状态的,没有副作用的函数,没有互斥锁。然而,它确实使用了大量的内存(我不知道这是否相关)。

过去,我访问过太阳实验室,并收集了标准的“性能与线程数”曲线。由于此函数没有互斥锁,因此它有一个很好的图形,尽管随着线程数量的增加,垃圾回收开始发挥作用。经过一些垃圾回收调整,我能够使这条曲线几乎平坦。

我现在在英特尔硬件上做同样的实验。硬件有 4 个 CPU,每个 CPU 有 8 个内核,以及超线程。这给出了64个可用的处理器()。不幸的是,“性能与线程数”的曲线可以很好地缩放1,2,3个线程,上限为3个线程。经过3个线程后,我可以根据需要将任意数量的线程放入任务中,并且性能不会更好

尝试解决问题

我的第一个想法是,我一直很愚蠢,在某个地方引入了一些同步代码。通常,为了解决这个问题,我运行JConsole或JVisualVM,并查看线程堆栈跟踪。如果我有 64 个线程以 3 的速度运行,我预计其中 61 个线程会坐着等待进入互斥体。我没有找到这个。相反,我发现所有的线程都在运行:只是非常慢。

第二个想法是,也许时间框架带来了问题。我用一个虚拟函数替换了我的函数,这个函数使用AtomicLong只能算作十亿。这随着线程数量的增加而完美地扩展:我能够数到10亿倍,64个线程比1个线程快64倍。

我想(绝望开始了)也许垃圾回收需要很长时间,所以我调整了垃圾回收参数。虽然这改善了我的延迟变化,但它对吞吐量没有影响:我仍然有64个线程以我期望3运行的速度运行。

我已经下载了英特尔工具VTunes,但我的技能很弱:它是一个复杂的工具,我还不了解它。我订购了说明书:给自己一个有趣的圣诞礼物,但这有点太晚了,无法帮助我目前的问题

问题

  1. 我可以使用哪些工具(心理或软件)来提高我对正在发生的事情的理解?
  2. 除了互斥锁或垃圾回收之外,还有哪些机制可能会减慢我的代码速度?

答案 1

我有一段java代码(JDK 1.6.0._22,如果相关)

从那时起,已经有相当多的性能改进。我会尝试Java 6 update 37或Java 7 update 10。

但是,它确实会占用大量内存

这可能意味着您访问数据的方式可能很重要。访问主内存中的数据可能比主缓存中的慢 20+x。这意味着您必须保守地访问数据,并充分利用您访问的每条新数据。

在3个线程之后,我可以把尽可能多的线程放到任务中,而且性能也没有好转,相反,我发现所有的线程都在运行:只是非常慢。

这表明您正在最大限度地使用资源。考虑到您使用的内存量,最有可能被最大化的资源是 CPU 到主内存桥。我怀疑你有一个64线程的桥!这意味着您应该考虑可能使用更多CPU的方法,但要改进访问内存的方式(更少随机和更按顺序),并在这样做时减少卷(尽可能使用更紧凑的类型)。例如,我有“短两个小数位”类型,而不是浮点数,它可以使用一半的内存。

正如你所观察到的,当每个线程都在更新它自己的私有AtomicLong时,你得到了线性的可扩展性。这根本不会使用CPU到主内存桥接器。


从@Marko

Peter,您知道这些多核架构如何与内存配合使用吗?

没有我想要的那么多,因为这个问题对Java来说是不可见的。

每个核心都有独立的通道吗?

每个内核都有一个到主缓存的独立通道。对于外部缓存,每个缓存区域或 2-6 个缓存区域可能有一个通道,但在重负载下,您将发生大量冲突。

对于到主内存的桥接,有一个非常宽的通道。这有利于长顺序访问,但对于随机访问来说非常差。单个线程可以通过随机读取来最大化这一点(随机性足以使它们不适合外部缓存)

或者至少是独立的,只要没有碰撞?

一旦耗尽主缓存(L1 通常为 32 KB),它就会一直发生冲突。

因为否则缩放是一个大问题。

正如OP所示。大多数应用程序要么a)花费大量时间等待IO b)在小批量数据上分配计算。对大量数据进行分配计算是最糟糕的情况。

我处理这个问题的方法是在内存中排列我的数据结构以进行顺序访问。我使用堆外内存,这是一种痛苦,但让你完全控制布局。(我的源数据是为持久性而映射的内存)我通过顺序访问将数据流式传输进来,并尝试充分利用这些数据(即我最大限度地减少重复访问)即使使用16个内核,也很难假设所有这些内核都会得到有效使用,因为我在任何时候都有40 GB的源数据和大约80 GB的派生数据。

注意:高端 GPU 通过具有令人难以置信的高内存带宽来解决此问题。高端处理器可以获得250 GB /秒,而典型的CPU约为4-6 GB /秒。即便如此,它们更适合矢量化处理,并且它们引用的峰值性能可能几乎没有内存访问,例如曼德布洛特集。

http://www.nvidia.com/object/tesla-servers.html


答案 2

很多实验后,我发现JVM没有任何区别,但我也发现了JDump的强大功能。64 个线程中有 50 个位于下一行。

java.lang.Thread.State: RUNNABLE
    at java.util.Random.next(Random.java:189)
    at java.util.Random.nextInt(Random.java:239)
    at sun.misc.Hashing.randomHashSeed(Hashing.java:254)
    at java.util.HashMap.<init>(HashMap.java:255)
    at java.util.HashMap.<init>(HashMap.java:297)

Random.next 看起来像这样

 protected int next(int bits) {
    long oldseed, nextseed;
    AtomicLong seed = this.seed;
    do {
        oldseed = seed.get();
        nextseed = (oldseed * multiplier + addend) & mask;
    } while (!seed.compareAndSet(oldseed, nextseed));
    return (int)(nextseed >>> (48 - bits));
 }

最有趣的是,这不是一个明显的锁,所以我用来发现互斥锁的工具不起作用。

因此,看起来任何java哈希图的创建都会导致应用程序不再可扩展(我夸张但不多)。我的应用程序确实大量使用哈希映射,所以我想我要么重写哈希映射,要么重写应用程序。

我提出了一个单独的问题,看看如何处理这个问题。

感谢所有的帮助


推荐