17

问题

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

过去,我访问过 Sun 实验室并收集了标准的“性能与线程数”曲线。由于这个函数没有互斥体,它有一个很好的图表,尽管垃圾收集随着线程数量的增加而启动。经过一些垃圾收集调整后,我能够使这条曲线几乎平坦。

我现在正在英特尔硬件上做同样的实验。硬件有 4 个 CPU,每个 CPU 有 8 个内核和超线程。这给出了 64 个可用的处理器()。不幸的是,“性能与线程数”的曲线很好地适用于 1、2、3 个线程,并且上限为 3 个线程。在 3 个线程之后,我可以将任意数量的线程放入任务中,并且性能并没有变得更好

尝试解决问题

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

第二个想法是,也许时间框架正在引入问题。我用一个虚拟函数替换了我的函数,该函数使用 AtomicLong 可以计算到十亿。这与线程数完美地缩放:使用 64 线程比使用 1 线程快 64 倍,我能够数到十亿 10,000 倍。

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

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

问题

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

3 回答 3

11

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

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

但是它确实使用了大量的内存

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

在 3 个线程之后,我可以将任意数量的线程放入任务中,但性能并没有好转相反,我发现所有线程都在运行:只是非常慢。

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

正如您所观察到的,当每个线程更新它自己的私有 AtomicLong 时,您将获得线性可伸缩性。这根本不会使用 cpu 到主内存桥。


来自@Marko

彼得,你知道这些多核架构是如何与内存一起工作的吗?

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

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

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

对于通往主内存的桥梁,有一个非常宽的通道。这有利于长顺序访问,但对于随机访问非常不利。单个线程可以通过随机读取将其最大化(足够随机,它们不适合外部缓存)

或者至少在没有碰撞的情况下是独立的?

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

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

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

我处理这个问题的方法是将我的数据结构安排在内存中以进行顺序访问。我使用堆外内存,这很痛苦,但可以让您完全控制布局。(我的源数据是用于持久性的内存映射)我通过顺序访问将数据流式传输并尝试充分利用这些数据(即,我尽量减少重复访问)即使有 16 个内核,也很难假设所有内核都将被使用高效,因为我有 40 GB 的源数据我正在处理任何时间和大约 80 GB 的派生数据。

注意:高端 GPU 通过具有令人难以置信的高内存带宽来解决这个问题。高端处理器可以达到 250 GB/秒,而典型的 CPU 大约是 4-6 GB/秒。即便如此,它们更适合矢量化处理,并且它们引用的峰值性能可能很少有内存访问,例如 mandelbrot 集。

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

于 2012-12-20T09:41:12.200 回答
9

很多实验之后我发现 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 hashmaps 的创建都会导致应用程序停止可扩展(我夸大了但不多)。我的应用程序确实大量使用了 hashmap,所以我想我要么重写 hashmap,要么重写应用程序。

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

感谢所有的帮助

于 2012-12-22T13:47:23.177 回答
0

您可能会遇到分配墙,即:您的程序运行速度不会比对象分配快,这受内存带宽的限制。

于 2012-12-23T13:29:31.890 回答