提高性能一致性的方法

2022-09-01 00:10:36

在下面的示例中,一个线程通过字节缓冲区发送“消息”,这是消费者正在获取的。最佳性能非常好,但并不一致。

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;
        }
    }
}

打印同一数据块的计时。末尾的数字是对象的相对地址,显示它们每次都以相同的方式布置在缓存中。运行 10 个的更长测试表明,给定的组合会重复产生相同的性能。

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

每组缓冲区和计数器都经过三次测试,这些缓冲区似乎给出了类似的结果。所以我相信这些缓冲区在内存中的布局方式是我没有看到的。

有什么东西可以更频繁地提供更高的性能吗?它看起来像缓存冲突,但我看不到这种情况可能发生在哪里。

BTW:每秒数百万条消息,比任何人都可能需要的要多,但是了解如何使其始终保持快速是件好事。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

编辑:通过使用任务集,如果我锁定两个线程以更改同一内核,我可以使性能保持一致。

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中途。

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(家用PC)上得到了以下内容

注意:偏移量错误 8 倍。由于堆大小很小,JVM 不会像堆(但小于 32 GB)那样将引用乘以 8。

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无法扩展,因此您无法添加填充,因此我创建了一个我丢弃的对象。

    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之后,对象总是存在无法以最佳方式布局的风险。解决此问题的唯一方法可能是向原始类添加填充。:(


答案 1

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

使用您的代码并制作一些mod,我已经能够使性能保持一致(我的测试机器是Intel Core2 Quad CPU Q6600 2.4GHz w / Win7x64 - 所以不是很一样,但希望足够接近以获得相关结果)。我以两种不同的方式做到了这一点,这两种方式都有大致相同的效果。

首先,将缓冲区和计数器的创建移到 doTest 方法之外,以便它们仅创建一次,然后在每次测试过程中重复使用。现在你得到了一个分配,它很好地位于缓存中,并且性能是一致的。

获得相同重用但使用“不同”缓冲区/计数器的另一种方法是在执行Timing循环之后插入gc:

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

在这里,结果或多或少是相同的 - gc允许回收缓冲区/计数器,下一个分配最终重用相同的内存(至少在我的测试系统上),并且您最终在缓存中具有一致的性能(我还添加了实际地址的打印以验证相同位置的重用)。我的猜测是,如果没有清理导致重用,您最终会分配一个不适合缓存的缓冲区,并且您的性能在交换时会受到影响。我怀疑你可以按照分配顺序做一些奇怪的事情(比如你可以通过在缓冲区前面移动计数器分配来降低我的机器的性能),或者如果你不想从前面的循环中消除缓冲区,请在每次运行周围创建一些死空间来“清除”缓存。

最后,正如我所说,处理器缓存和内存布局的乐趣不是我的专业领域,所以如果解释是误导性的或错误的 - 很抱歉。


答案 2

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

读者:

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

作家:

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

推荐