Подтвердить что ты не робот

Способы улучшения согласованности производительности

В следующем примере один поток отправляет "сообщения" через 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

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

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

BTW: M/s - миллионы сообщений в секунду, и это больше, чем кому-либо, вероятно, потребуется, но было бы полезно понять, как сделать его последовательно быстрым.


EDIT: использование синхронизированного с wait и notify делает результат намного более последовательным. Но не быстрее.

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

EDIT: с помощью набора задач я могу сделать производительность согласованной, если я заблокирую два потока для изменения одного и того же ядра.

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

EDIT: Похоже, что запуск 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 ГГц)

Примечание: смещения неверны в 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

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

4b9b3361

Ответ 1

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

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

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

Другой способ получить одно и то же повторное использование, но с "разными" буферами/счетчиками - вставить gc после цикла performTiming:

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

Ответ 3

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

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

Ответ 4

EDIT: Похоже, что запуск GC приведет к изменению поведения. Эти показать повторное тестирование на том же буфере + счетчики с ручным триггером GC на полпути.

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

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

ИЗМЕНИТЬ

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

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

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

Ответ 5

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

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

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

Ответ 6

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