Пути улучшения согласованности производительности - PullRequest
45 голосов
/ 01 ноября 2011

В следующем примере один поток отправляет «сообщения» через ByteBuffer, который принимает потребитель.Наилучшая производительность очень хорошая, но не согласованная.

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

Каждый набор буферов и счетчик тестируются три раза, и эти буферы дают похожие результаты.Так что я верю, что есть что-то в том, как эти буферы расположены в памяти, я не вижу.

Есть ли что-нибудь, что может дать более высокую производительность чаще?Это похоже на коллизию кеша, но я не вижу, где это может происходить.

Кстати: 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 для печати используемого идентификатора ядра, я получаю следующее на i7 3,8 ГГц(домашний ПК)

1028 * Примечание: смещения неправильны с коэффициентом 8. Поскольку размер кучи был мал, JVM не умножать ссылку на 8, как это делает с кучей, которая больше (номенее 32 ГБ).
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ГГц 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

К сожалению, всегда существует риск того, что после ГХ объекты не будут оптимально разложены.Единственным способом решения этой проблемы может быть добавление отступов к исходному классу.(

Ответы [ 6 ]

24 голосов
/ 05 ноября 2011

Я не эксперт в области кеширования процессора, но я подозреваю, что ваша проблема - это, по сути, проблема с кешем или какая-то другая проблема с разметкой памяти.Повторное выделение буферов и счетчиков без очистки старых объектов может периодически вызывать очень плохую компоновку кэша, что может привести к нестабильной производительности.

Использование вашего кода и создание нескольких модов, которые у меня естья смог обеспечить стабильную производительность (мой тестовый компьютер - Intel Core2 Quad CPU Q6600 2,4 ГГц с Win7x64 - поэтому не совсем то же самое, но, надеюсь, достаточно близко, чтобы получить соответствующие результаты).Я сделал это двумя разными способами, оба из которых имеют примерно одинаковый эффект.

Сначала переместите создание буферов и счетчиков за пределы метода doTest, чтобы они создавались только один раз, а затем повторно использовали для каждогопройти тест.Теперь вы получаете одно выделение, оно хорошо размещается в кеше, а производительность стабильна.

Еще один способ получить то же повторное использование, но с «другими» буферами / счетчиками, - это вставить gc после цикла executeTiming:

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

Здесь результат более или менее одинаков - gc позволяет восстанавливать буферы / счетчики, и следующее выделение заканчивается повторным использованием той же памяти (по крайней мере, в моей тестовой системе), и вы в конечном итоге получаетекэш с постоянной производительностью (я также добавил печать реальных адресов, чтобы проверить повторное использование тех же мест).Я предполагаю, что без очистки, ведущей к повторному использованию, вы в конечном итоге получите выделенный буфер, который не помещается в кеш, и ваша производительность страдает при его замене. Я подозреваю, что вы могли бы сделать некоторые странные вещи с порядком распределения(например, вы можете ухудшить производительность на моем компьютере, переместив распределение счетчиков перед буферами) или создавая некоторое мертвое пространство вокруг каждого прогона, чтобы «очистить» кэш, если вы не хотите удалять буферы из предыдущего цикла.

Наконец, как я уже сказал, кэш процессора и удовольствие от разметки памяти - не моя область знаний, поэтому, если объяснения вводят в заблуждение или ошибочны - извините за это.

8 голосов
/ 01 ноября 2011

вы заняты ожиданием.это всегда плохая идея в коде пользователя.

читатель:

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

писатель:

while (wc - readCount.get() > 0) ;
6 голосов
/ 07 ноября 2011

Как вы на самом деле прикрепляете свои потоки к ядрам?taskset - не лучший способ привязать потоки к ядрам, поскольку он просто прикрепляет процессы к ядрам - и все его потоки будут совместно использовать эти ядра.Напомним, у java есть много внутренних потоков для собственных нужд, поэтому все они будут конкурировать с ядрами, с которыми вы их связываете.

Чтобы получить более согласованные результаты, вы можете использовать JNA для вызова sched_setaffinity () только из тех потоков, которые вы используете.нужно.Он будет привязывать только ваши тестовые потоки к конкретным ядрам, в то время как другие потоки Java будут распространяться на другие свободные ядра, что будет меньше влиять на поведение вашего кода.

Кстати, у меня есть похожие проблемы с нестабильной производительностью, в то время какбенчмаркинг высоко оптимизированного параллельного кода.Похоже, где слишком много вещей, которые могут существенно повлиять на производительность, когда она близка к аппаратным ограничениям.Вы должны как-то настроить свою ОС, чтобы дать своему коду возможность сделать его лучше, или просто использовать много экспериментов и использовать математику, чтобы получить средние значения и доверительные интервалы.

6 голосов
/ 02 ноября 2011

РЕДАКТИРОВАТЬ: Похоже, что запуск GC изменит поведение.Они показывают повторное тестирование на одном и том же буфере + счетчиках с ручным запуском GC на полпути.

GC означает достижение безопасной точки, что означает, что все потоки прекратили выполнение байт-кода, и потоки GC должны работать.Это может иметь различные побочные эффекты.Например, в отсутствие какого-либо явного соответствия процессору, вы можете возобновить выполнение на другом ядре, или строки кэша могли быть обновлены.Можете ли вы отследить, на каких ядрах работают ваши потоки?

Какие это процессоры?Делали ли вы что-нибудь с управлением питанием, чтобы предотвратить его падение в более низкие состояния p и / или c?Возможно, 1 поток запланирован на ядре, которое находилось в другом состоянии p, следовательно, показывает другой профиль производительности.

EDIT

Я попытался запустить тест на рабочей станциипри запуске x64 linux с двумя слегка старыми четырехъядерными ксенонами (E5504), это обычно согласуется в пределах прогона (~ 17-18M / s), при этом время выполнения намного медленнее, что обычно соответствует миграции потоковЯ не планировал это строго.Поэтому кажется, что ваша проблема может быть связана с архитектурой процессора.Вы упоминаете, что используете i7 на частоте 4,6 ГГц, это опечатка?Я думал, что i7 достиг максимума в 3,5 ГГц с 3,9 ГГц турбо-режимом (с более ранней версией от 3,3 ГГц до 3,6 ГГц турбо).В любом случае, вы уверены, что не видите, что артефакт турбо-режима включается и выпадает?Вы можете попробовать повторить тест с отключенным турбо, чтобы быть уверенным.

Несколько других точек

  • все значения отступов равны 0, вы уверены, что нет особой обработкибыть приравненным к неинициализированным ценностям?вы могли бы рассмотреть возможность использования опции LogCompilation, чтобы понять, как JIT обрабатывает этот метод
  • Intel VTune бесплатна для 30-дневной оценки, если это проблема строки кэша, вы можете использоватьчтобы определить в чем проблема на вашем хосте
6 голосов
/ 01 ноября 2011

Как общий подход к анализу производительности:

  • Попробуйте jconsole .Запустите ваше приложение, и пока оно работает, введите jconsole в отдельном окне терминала.Это вызовет графический интерфейс Java Console, который позволяет вам подключаться к работающей JVM, и видеть показатели производительности, использование памяти, количество потоков и состояние и т. Д.
  • В основном вам придется выяснить,корреляция между колебаниями скорости и тем, что, как вы видите, делает JVM.Также может быть полезно вызвать диспетчер задач и посмотреть, действительно ли ваша система просто занята другими делами (подкачкой на диск из-за нехватки памяти, занятостью с тяжелой фоновой задачей и т. Д.) И поставить ее рядом.сторона с окном jconsole.
  • Еще одна альтернатива - запуск JVM с опцией -Xprof, которая выводит относительное время, потраченное в различных методах для каждого потока.Ex.java -Xprof [your class file]
  • Наконец, есть также JProfiler , но это коммерческий инструмент, если это важно для вас.
2 голосов
/ 08 ноября 2011

Конечно, при полной сборке GC может возникнуть некоторая несогласованность, но это не так часто.Попробуйте изменить размер стека (Xss), скажем, 32M, и посмотрите, поможет ли это.Кроме того, попробуйте очистить 2 буфера в конце каждого теста, чтобы GC еще легче было узнать, что содержимое может быть собрано.Интересно, что вы использовали thread.stop (), который устарел и абсолютно не рекомендуется.Я бы тоже предложил это изменить.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...