提高性能一致性的方法

在以下示例中,一个线程正在通过消费者正在使用的ByteBuffer发送“消息”。 最好的performance是非常好的,但不一致。

public class Main { public static void main(String... args) throws IOException { for (int i = 0; i < 10; i++) doTest(); } public static void doTest() { final ByteBuffer writeBuffer = ByteBuffer.allocateDirect(64 * 1024); final ByteBuffer readBuffer = writeBuffer.slice(); final AtomicInteger readCount = new PaddedAtomicInteger(); final AtomicInteger writeCount = new PaddedAtomicInteger(); for(int i=0;i<3;i++) performTiming(writeBuffer, readBuffer, readCount, writeCount); System.out.println(); } private static void performTiming(ByteBuffer writeBuffer, final ByteBuffer readBuffer, final AtomicInteger readCount, final AtomicInteger writeCount) { writeBuffer.clear(); readBuffer.clear(); readCount.set(0); writeCount.set(0); Thread t = new Thread(new Runnable() { @Override public void run() { byte[] bytes = new byte[128]; while (!Thread.interrupted()) { int rc = readCount.get(), toRead; while ((toRead = writeCount.get() - rc) <= 0) ; for (int i = 0; i < toRead; i++) { byte len = readBuffer.get(); if (len == -1) { // rewind. readBuffer.clear(); // rc++; } else { int num = readBuffer.getInt(); if (num != rc) throw new AssertionError("Expected " + rc + " but got " + num) ; rc++; readBuffer.get(bytes, 0, len - 4); } } readCount.lazySet(rc); } } }); t.setDaemon(true); t.start(); Thread.yield(); long start = System.nanoTime(); int runs = 30 * 1000 * 1000; int len = 32; byte[] bytes = new byte[len - 4]; int wc = writeCount.get(); for (int i = 0; i < runs; i++) { if (writeBuffer.remaining() < len + 1) { // reader has to catch up. while (wc - readCount.get() > 0) ; // rewind. writeBuffer.put((byte) -1); writeBuffer.clear(); } writeBuffer.put((byte) len); writeBuffer.putInt(i); writeBuffer.put(bytes); writeCount.lazySet(++wc); } // reader has to catch up. while (wc - readCount.get() > 0) ; t.interrupt(); t.stop(); long time = System.nanoTime() - start; System.out.printf("Message rate was %.1f M/s offsets %d %d %d%n", runs * 1e3 / time , addressOf(readBuffer) - addressOf(writeBuffer) , addressOf(readCount) - addressOf(writeBuffer) , addressOf(writeCount) - addressOf(writeBuffer) ); } // assumes -XX:+UseCompressedOops. public static long addressOf(Object... o) { long offset = UNSAFE.arrayBaseOffset(o.getClass()); return UNSAFE.getInt(o, offset) * 8L; } public static final Unsafe UNSAFE = getUnsafe(); public static Unsafe getUnsafe() { try { Field field = Unsafe.class.getDeclaredField("theUnsafe"); field.setAccessible(true); return (Unsafe) field.get(null); } catch (Exception e) { throw new AssertionError(e); } } private static class PaddedAtomicInteger extends AtomicInteger { public long p2, p3, p4, p5, p6, p7; public long sum() { // return 0; return p2 + p3 + p4 + p5 + p6 + p7; } } } 

打印同一块数据的时间。 最后的数字是每次显示在caching中放置的对象的相对地址。 运行更长的10次testing表明,给定的组合会重复产生相同的性能。

 Message rate was 63.2 M/s offsets 136 200 264 Message rate was 80.4 M/s offsets 136 200 264 Message rate was 80.0 M/s offsets 136 200 264 Message rate was 81.9 M/s offsets 136 200 264 Message rate was 82.2 M/s offsets 136 200 264 Message rate was 82.5 M/s offsets 136 200 264 Message rate was 79.1 M/s offsets 136 200 264 Message rate was 82.4 M/s offsets 136 200 264 Message rate was 82.4 M/s offsets 136 200 264 Message rate was 34.7 M/s offsets 136 200 264 Message rate was 39.1 M/s offsets 136 200 264 Message rate was 39.0 M/s offsets 136 200 264 

每组缓冲区和计数器都进行了三次testing,这些缓冲区似乎给出了类似的结果。 所以我相信有一些关于这些缓冲区铺在记忆中的方法我没有看到。

有什么可以提高性能的吗? 它看起来像一个caching冲突,但我不能看到这可能发生的地方。

顺便说一句: M/s是每秒数百万条消息,比任何人都可能需要更多,但它是理解如何使其始终如一的快速。


编辑:使用同步等待和通知使结果更加一致。 但不是更快。

 Message rate was 6.9 M/s Message rate was 7.8 M/s Message rate was 7.9 M/s Message rate was 6.7 M/s Message rate was 7.5 M/s Message rate was 7.7 M/s Message rate was 7.3 M/s Message rate was 7.9 M/s Message rate was 6.4 M/s Message rate was 7.8 M/s 

编辑:通过使用任务设置,我可以使性能一致,如果我locking两个线程来改变相同的核心。

 Message rate was 35.1 M/s offsets 136 200 216 Message rate was 34.0 M/s offsets 136 200 216 Message rate was 35.4 M/s offsets 136 200 216 Message rate was 35.6 M/s offsets 136 200 216 Message rate was 37.0 M/s offsets 136 200 216 Message rate was 37.2 M/s offsets 136 200 216 Message rate was 37.1 M/s offsets 136 200 216 Message rate was 35.0 M/s offsets 136 200 216 Message rate was 37.1 M/s offsets 136 200 216 If I use any two logical threads on different cores, I get the inconsistent behaviour Message rate was 60.2 M/s offsets 136 200 216 Message rate was 68.7 M/s offsets 136 200 216 Message rate was 55.3 M/s offsets 136 200 216 Message rate was 39.2 M/s offsets 136 200 216 Message rate was 39.1 M/s offsets 136 200 216 Message rate was 37.5 M/s offsets 136 200 216 Message rate was 75.3 M/s offsets 136 200 216 Message rate was 73.8 M/s offsets 136 200 216 Message rate was 66.8 M/s offsets 136 200 216 

编辑:看来,触发GC将改变行为。 这些显示在半手动触发GC的同一个缓冲液+计数器上重复testing。

 faster after GC Message rate was 27.4 M/s offsets 136 200 216 Message rate was 27.8 M/s offsets 136 200 216 Message rate was 29.6 M/s offsets 136 200 216 Message rate was 27.7 M/s offsets 136 200 216 Message rate was 29.6 M/s offsets 136 200 216 [GC 14312K->1518K(244544K), 0.0003050 secs] [Full GC 1518K->1328K(244544K), 0.0068270 secs] Message rate was 34.7 M/s offsets 64 128 144 Message rate was 54.5 M/s offsets 64 128 144 Message rate was 54.1 M/s offsets 64 128 144 Message rate was 51.9 M/s offsets 64 128 144 Message rate was 57.2 M/s offsets 64 128 144 and slower Message rate was 61.1 M/s offsets 136 200 216 Message rate was 61.8 M/s offsets 136 200 216 Message rate was 60.5 M/s offsets 136 200 216 Message rate was 61.1 M/s offsets 136 200 216 [GC 35740K->1440K(244544K), 0.0018170 secs] [Full GC 1440K->1302K(244544K), 0.0071290 secs] Message rate was 53.9 M/s offsets 64 128 144 Message rate was 54.3 M/s offsets 64 128 144 Message rate was 50.8 M/s offsets 64 128 144 Message rate was 56.6 M/s offsets 64 128 144 Message rate was 56.0 M/s offsets 64 128 144 Message rate was 53.6 M/s offsets 64 128 144 

编辑:使用@ BegemoT的库打印使用的核心ID我得到以下3.8 GHz i7(家用电脑)

注意:偏移量不正确的因子为8.由于堆大小很小,JVM不会像8位堆一样占用较大(但小于32 GB)的堆。

 writer.currentCore() -> Core[#0] reader.currentCore() -> Core[#5] Message rate was 54.4 M/s offsets 3392 3904 4416 writer.currentCore() -> Core[#0] reader.currentCore() -> Core[#6] Message rate was 54.2 M/s offsets 3392 3904 4416 writer.currentCore() -> Core[#0] reader.currentCore() -> Core[#5] Message rate was 60.7 M/s offsets 3392 3904 4416 writer.currentCore() -> Core[#0] reader.currentCore() -> Core[#5] Message rate was 25.5 M/s offsets 1088 1600 2112 writer.currentCore() -> Core[#0] reader.currentCore() -> Core[#5] Message rate was 25.9 M/s offsets 1088 1600 2112 writer.currentCore() -> Core[#0] reader.currentCore() -> Core[#5] Message rate was 26.0 M/s offsets 1088 1600 2112 writer.currentCore() -> Core[#0] reader.currentCore() -> Core[#5] Message rate was 61.0 M/s offsets 1088 1600 2112 writer.currentCore() -> Core[#0] reader.currentCore() -> Core[#5] Message rate was 61.8 M/s offsets 1088 1600 2112 writer.currentCore() -> Core[#0] reader.currentCore() -> Core[#5] Message rate was 60.7 M/s offsets 1088 1600 2112 

您可以看到正在使用相同的逻辑线程,但性能会在运行之间变化,但不在运行中(在运行中使用相同的对象)


我发现了这个问题。 这是一个内存布局问题,但我可以看到一个简单的方法来解决它。 ByteBuffer不能被扩展,所以你不能添加填充,所以我创build了一个我放弃的对象。

  final ByteBuffer writeBuffer = ByteBuffer.allocateDirect(64 * 1024); final ByteBuffer readBuffer = writeBuffer.slice(); new PaddedAtomicInteger(); final AtomicInteger readCount = new PaddedAtomicInteger(); final AtomicInteger writeCount = new PaddedAtomicInteger(); 

没有这个额外的填充(未使用的物体),结果看起来像3.8 GHz的i7。

 Message rate was 38.5 M/s offsets 3392 3904 4416 Message rate was 54.7 M/s offsets 3392 3904 4416 Message rate was 59.4 M/s offsets 3392 3904 4416 Message rate was 54.3 M/s offsets 1088 1600 2112 Message rate was 56.3 M/s offsets 1088 1600 2112 Message rate was 56.6 M/s offsets 1088 1600 2112 Message rate was 28.0 M/s offsets 1088 1600 2112 Message rate was 28.1 M/s offsets 1088 1600 2112 Message rate was 28.0 M/s offsets 1088 1600 2112 Message rate was 17.4 M/s offsets 1088 1600 2112 Message rate was 17.4 M/s offsets 1088 1600 2112 Message rate was 17.4 M/s offsets 1088 1600 2112 Message rate was 54.5 M/s offsets 1088 1600 2112 Message rate was 54.2 M/s offsets 1088 1600 2112 Message rate was 55.1 M/s offsets 1088 1600 2112 Message rate was 25.5 M/s offsets 1088 1600 2112 Message rate was 25.6 M/s offsets 1088 1600 2112 Message rate was 25.6 M/s offsets 1088 1600 2112 Message rate was 56.6 M/s offsets 1088 1600 2112 Message rate was 54.7 M/s offsets 1088 1600 2112 Message rate was 54.4 M/s offsets 1088 1600 2112 Message rate was 57.0 M/s offsets 1088 1600 2112 Message rate was 55.9 M/s offsets 1088 1600 2112 Message rate was 56.3 M/s offsets 1088 1600 2112 Message rate was 51.4 M/s offsets 1088 1600 2112 Message rate was 56.6 M/s offsets 1088 1600 2112 Message rate was 56.1 M/s offsets 1088 1600 2112 Message rate was 46.4 M/s offsets 1088 1600 2112 Message rate was 46.4 M/s offsets 1088 1600 2112 Message rate was 47.4 M/s offsets 1088 1600 2112 

与丢弃的被填塞的对象。

 Message rate was 54.3 M/s offsets 3392 4416 4928 Message rate was 53.1 M/s offsets 3392 4416 4928 Message rate was 59.2 M/s offsets 3392 4416 4928 Message rate was 58.8 M/s offsets 1088 2112 2624 Message rate was 58.9 M/s offsets 1088 2112 2624 Message rate was 59.3 M/s offsets 1088 2112 2624 Message rate was 59.4 M/s offsets 1088 2112 2624 Message rate was 59.0 M/s offsets 1088 2112 2624 Message rate was 59.8 M/s offsets 1088 2112 2624 Message rate was 59.8 M/s offsets 1088 2112 2624 Message rate was 59.8 M/s offsets 1088 2112 2624 Message rate was 59.2 M/s offsets 1088 2112 2624 Message rate was 60.5 M/s offsets 1088 2112 2624 Message rate was 60.5 M/s offsets 1088 2112 2624 Message rate was 60.5 M/s offsets 1088 2112 2624 Message rate was 60.5 M/s offsets 1088 2112 2624 Message rate was 60.9 M/s offsets 1088 2112 2624 Message rate was 60.6 M/s offsets 1088 2112 2624 Message rate was 59.6 M/s offsets 1088 2112 2624 Message rate was 60.3 M/s offsets 1088 2112 2624 Message rate was 60.5 M/s offsets 1088 2112 2624 Message rate was 60.9 M/s offsets 1088 2112 2624 Message rate was 60.5 M/s offsets 1088 2112 2624 Message rate was 60.5 M/s offsets 1088 2112 2624 Message rate was 60.7 M/s offsets 1088 2112 2624 Message rate was 61.6 M/s offsets 1088 2112 2624 Message rate was 60.8 M/s offsets 1088 2112 2624 Message rate was 60.3 M/s offsets 1088 2112 2624 Message rate was 60.7 M/s offsets 1088 2112 2624 Message rate was 58.3 M/s offsets 1088 2112 2624 

不幸的是,在GC之后总是存在风险最大化的问题。 解决此问题的唯一方法可能是将填充添加到原始类。 🙁

我不是处理器caching领域的专家,但我怀疑你的问题本质上是caching问题或其他内存布局问题。 重复分配缓冲区和计数器而不清理旧对象可能会导致您定期获取非常糟糕的caching布局,这可能会导致您的性能不一致。

使用你的代码和做一些MODS我已经能够使性能一致(我的testing机是英特尔酷睿2四核CPU Q6600 2.4GHz W / Win7x64 – 所以不完全相同,但希望足够接近,有相关的结果)。 我用两种不同的方式做了这两个具有大致相同的效果。

首先,移动doTest方法之外的缓冲区和计数器的创build,以便它们仅被创build一次,然后在每次testing通过时重复使用。 现在你得到了一个分配,它在caching中很好,并且性能是一致的。

另一种获得相同的重用但具有“不同的”缓冲器/计数器的方法是在performTiming循环之后插入一个gc:

 for ( int i = 0; i < 3; i++ ) performTiming ( writeBuffer, readBuffer, readCount, writeCount ); System.out.println (); System.gc (); 

这里的结果或多或less是相同的 – gc让缓冲区/计数器被回收,并且下一个分配结束时重复使用相同的内存(至less在我的testing系统上),并且最终在caching中保持一致的性能(我也添加了打印实际地址以validation相同位置的重用)。 我的猜测是,如果没有清理导致重用,你最终会得到一个缓冲区分配,不适合caching,你的性能受到影响,因为它是交换。我怀疑你可以做一些奇怪的事情与分配的顺序(就像你可以通过在缓冲区前面移动计数器分配,使我的机器的性能变差),或者在每个运行周围创build一些死空间来清除caching,如果你不想从先前的循环中删除缓冲区。

最后,正如我所说,处理器caching和内存布局的乐趣不是我的专业领域,所以如果解释是误导或错误的 – 对此感到遗憾。

你正在等待。 这在用户代码中总是一个坏主意。

读者:

 while ((toRead = writeCount.get() - rc) <= 0) ; 

作家:

 while (wc - readCount.get() > 0) ; 

作为性能分析的一般方法:

  • 试试jconsole 。 启动您的应用程序,并在运行时在单独的terminal窗口中键入jconsole 。 这将启动Java控制台GUI,它允许您连接到正在运行的JVM,并查看性能指标,内存使用情况,线程数量和状态等。
  • 基本上你必须弄清楚速度的变化与你看到的JVM之间的关系。 调出你的任务pipe理器也是有帮助的,看看你的系统是否真的只是忙着做其他的事情(由于内存不足,分配给磁盘,忙于繁重的后台任务等)一边与jconsole窗口。
  • 另一种方法是使用-Xprof选项启动JVM,该选项可以每个线程的基础上输出在各种方法中花费的相对时间。 防爆。 java -Xprof [your class file]
  • 最后,还有JProfiler ,但它是一个商业工具,如果这对你很重要。

编辑:看来,触发GC将改变行为。 这些显示在半手动触发GC的同一个缓冲液+计数器上重复testing。

GC意味着达到一个安全点,这意味着所有的线程已经停止执行字节码和GC线程有工作要做。 这可能会有各种副作用。 例如,如果没有任何显式的cpu关联性,则可能会重新启动其他内核上的执行,或者可能已经刷新了高速caching行。 你能跟踪你的线程在哪个核心上运行?

这些是什么CPU? 你有没有做过关于电源pipe理的事情,以防止它们进入较低的p和/或c状态? 也许1个线程正在调度到一个处于不同p状态的核心上,因此显示了不同的性能configuration文件。

编辑

我尝试在运行x64 linux的工作站上运行testing,使用2个稍微旧的quadcore Xeon(E5504),它在运行(〜17-18M / s)内通常是一致的,运行速度慢得多,通常与线程迁移相对应。 我没有把这个严格的阴谋。 所以看来你的问题可能是CPU体系结构特定的。 你提到你在4.6GHz运行一个i7,是一个错字? 我认为i7最高频率为3.5GHz,采用3.9Ghz的Turbo模式 (3.3GHz至3.6GHz的较早版本)。 无论哪种方式,你确定你没有看到一个涡轮模式踢在那么退出的人工制品? 你可以尝试用turbo禁用来重复testing。

其他几点

  • 填充值都是0,你确定没有对未初始化值进行特殊处理吗? 您可以考虑使用LogCompilation选项来了解JIT如何处理该方法
  • 英特尔VTune是免费的30天评估,如果这是一个caching线问题,那么你可以用它来确定什么是你的主机上的问题

你怎么实际上把你的线程连接到核心? taskset并不是将内核固定到内核的最佳方式,因为它只是将内核引脚连接到内核,而其所有线程将共享这些内核。 回想一下,java有很多内部线程来满足自己的需求,所以它们将在你将绑定到的核心上进行竞争。

为了获得更一致的结果,您可以使用JNA仅从您需要的线程调用sched_setaffinity()。 它只会将您的基准testing线程固定到精确的内核,而其他java线程将散布在其他空闲内核上,对您的代码行为影响较小。

顺便说一下,我有类似的问题与不稳定的性能,而基准高度优化的并发代码。 看起来好像太多的东西在接近硬件限制的情况下,会剧烈地影响性能。 你应该调整你的操作系统,让你的代码可能做到最好,或者只是使用很多的实验,并使用math来获得平均值和置信区间。

当一个完整的GC运行时肯定会引入一些不一致的情况,但这并不常见。 尝试修改堆栈大小(Xss)说32M,看看是否有帮助。 另外,请在每次testing结束时尝试清除2个缓冲区,以便GC更容易知道可以收集内容。 有趣的是,您已经使用了不推荐使用的thread.stop(),绝对不推荐。 我会build议改变这一点。