除了互斥锁或垃圾收集之外的哪些机制可以减慢我的multithreadingjava程序?

问题

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

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

我现在在英特尔硬件上做同样的实验。 硬件有4个CPU,每个CPU有8个内核和超线程。 这给出了64个availableProcessors()。 不幸的是,“性能与线程数”的曲线很好地适用于1,2,3线程和3线程的大写。 在3个线程之后,我可以根据需要添加任意数量的线程,并且性能不会更好

尝试解决问题

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

第二个想法是,时间框架可能引入了问题。 我用一个虚拟函数替换了我的函数,使用AtomicLong计算到十亿。 这与线程数量相当精确:使用64个线程比使用1个线程,我能够快速计算到10倍10,000倍。

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

我已经下载了英特尔工具VTunes,但我的技巧很弱:它是一个复杂的工具,我还不明白。 我订购了说明书:给自己一个有趣的圣诞礼物,但是现在有点太晚了

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

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

从那以后,已经有很多性能改进。 我会尝试Java 6更新37或Java 7更新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 /秒。 即便如此,它们更适合于矢量化处理,并且它们引用的峰值性能可能几乎没有存储器访问,例如mandelbrot集。

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

很多实验后来我发现JVM没什么区别,但我也发现了JDump的强大function。 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.(HashMap.java:255) at java.util.HashMap.(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的创建都会导致应用程序停止扩展(我夸大但不多)。 我的应用程序确实大量使用了hashmaps,所以我想我要么重写hashmap要么重写应用程序。

我正在提出一个单独的问题,看看如何解决这个问题。

谢谢你的帮助

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