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

Какие механизмы, кроме мьютексов или сбор мусора, могут замедлить мою многопоточную java-программу?

Проблема

У меня есть кусок java-кода (JDK 1.6.0._22, если это уместно), который реализует свободную функцию без состояния без побочных эффектов без каких-либо мьютексов. Однако он использует много памяти (я не знаю, насколько это важно).

В прошлом я посетил Sun Laboratories и собрал стандартную кривую "производительность против числа потоков". Поскольку эта функция не имеет мьютексов, у нее есть хороший граф, хотя сбор мусора взлетел по мере увеличения количества потоков. После некоторой настройки сбора мусора я смог сделать эту кривую почти плоской.

Теперь я делаю тот же эксперимент на оборудовании Intel. Аппаратное обеспечение имеет 4 процессора каждый с 8 ядрами и гиперпоточность. Это дает 64 доступныхпроцессора(). К сожалению, кривая "производительность против числа потоков" хорошо масштабируется для 1, 2, 3 нитей и колпачков на 3 потоках. После 3 потоков я могу поставить столько потоков, сколько хочу, и производительность не улучшится

Попытки исправить проблему

Моя первая мысль заключалась в том, что я был глупым и где-то вводил какой-то синхронизированный код. Обычно для решения этой проблемы я запускаю JConsole или JVisualVM и смотрю на стек стека. Если у меня есть 64 потока, работающих со скоростью 3, я ожидаю, что 61 из них будет сидеть, ожидая входа в мьютекс. Я этого не нашел. Вместо этого я нашел все потоки: очень медленно.

Вторая мысль заключалась в том, что, возможно, временная структура вводит проблемы. Я заменил свою функцию фиктивной функцией, которая насчитывает миллиард, используя AtomicLong. Это прекрасно масштабируется с количеством потоков: я смог сосчитать до миллиарда 10 000 раз в 64 раза быстрее с 64 потоками, чем с 1 потоком.

Я подумал (отчаяние), возможно, сбор мусора занимает действительно очень много времени, поэтому я изменил параметры сбора мусора. Хотя это улучшило мою вариацию латентности, это не повлияло на пропускную способность: у меня все еще есть 64 потока, работающих на скорости, которую я ожидаю, чтобы 3 выполнялись.

Я загрузил инструмент Intel VTunes, но мое умение с ним слабое: это сложный инструмент, и я пока этого не понимаю. У меня есть инструкция по заказу: интересный рождественский подарок для меня, но это слишком поздно, чтобы помочь моей текущей проблеме.

Вопрос

  • Какие инструменты (умственные или программные) можно использовать для улучшения понимания того, что происходит?
  • Какие механизмы, кроме мьютексов или сбора мусора, могут замедлить мой код?
4b9b3361

Ответ 1

Много экспериментов позже я обнаружил, что JVM не имеет никакого значения, но я также обнаружил мощь JDump. 50 из 64 потоков были на следующей строке.

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.<init>(HashMap.java:255)
    at java.util.HashMap.<init>(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 заставляет приложения перестать быть масштабируемыми (я преувеличиваю, но не очень). Мое приложение сильно использует хэшмапы, поэтому, я думаю, я либо переписываю хэш-карту, либо переписываю приложение.

Я поднимаю отдельный вопрос, чтобы понять, как с этим справиться.

Спасибо за помощь

Ответ 2

У меня есть кусок java-кода (JDK 1.6.0._22, если это необходимо)

С тех пор были довольно значительные улучшения в производительности. Я бы попробовал обновление Java 6 update 37 или Java 7 10.

Однако он использует много памяти

Это может означать, что важно иметь доступ к вашим данным. Доступ к данным в основной памяти может быть на 20 + x медленнее, чем в вашем основном кеше. Это означает, что вам необходимо получить доступ к данным консервативно и максимально использовать каждую часть новых данных, к которым вы обращаетесь.

После 3 потоков я могу задать столько потоков, сколько захочу, и производительность не улучшится Вместо этого я нашел все потоки: очень медленно.

Это предполагает, что вы используете для этого ресурс максимум. Самый вероятный ресурс, который должен быть максимальным, учитывая объем используемой памяти, - это процессор для основного моста памяти. Я подозреваю, что у вас есть один мост для 64 потоков! Это означает, что вам следует рассмотреть способы, которые могут использовать больше процессоров, но улучшают доступ к памяти (менее случайным образом и более последовательно) и уменьшают объемы при использовании (при необходимости используйте более компактные типы). например У меня есть тип "short with two decimal places" вместо float, который может использовать половину используемой памяти.

Как вы заметили, когда каждый поток обновляет свой собственный AtomicLong, вы получаете линейную масштабируемость. Это не будет использовать процессор для основного моста памяти вообще.


Из @Marko

Питер, у вас есть идея, как эти многоярусные архитектуры работают с памятью? В любом случае?

Не так много, как хотелось бы, поскольку эта проблема не видна Java.

Есть ли у каждого ядра независимый канал?

Каждое ядро ​​имеет независимый канал для первичного кеша. Для внешнего кеша может быть канал для каждой или 2-6 кеш-областей, но при большой нагрузке вы столкнетесь с большим количеством столкновений.

Для моста в основной памяти есть один очень широкий канал. Это способствует длительному последовательному доступу, но очень плохо для случайного доступа. Один поток может максимизировать это со случайными чтениями (достаточно случайными, они не подходят во внешнем кеше)

Или, по крайней мере, независимый, при отсутствии столкновений?

Как только вы исчерпаете первичный кеш (L1, как правило, 32 КБ), он полностью конфликтует.

Потому что в противном случае масштабирование является большой проблемой.

Как показывает OP. Большинство приложений либо a) проводят значительную часть времени, ожидая ввода-вывода b) делает выделение вычислений на небольших партиях данных. Выполнение расчета вычислений по большим объемам данных является наихудшим случаем senario.

То, как я это делаю, - упорядочить структуры данных в памяти для последовательного доступа. Я использую память кучи, которая является болью, но дает вам полный контроль над планировкой. (Мои исходные данные представляют собой карту памяти для сохранения). Я передаю данные с помощью последовательного доступа и стараюсь максимально использовать эти данные (т.е. Минимизирую повторный доступ). Даже тогда с 16 ядрами трудно предположить, что все они будут использоваться эффективно, поскольку у меня есть 40 ГБ исходных данных, над которыми я работаю в любой момент времени, и около 80 ГБ полученных данных.

Примечание. Высокопроизводительные графические процессоры решают эту проблему, имея невероятно высокую пропускную способность памяти. Процессор верхнего уровня может получить 250 ГБ/с, тогда как типичный процессор составляет около 4-6 ГБ/с. Тем не менее, они лучше подходят для векторизованной обработки, и их цитированная пиковая производительность, вероятно, будет иметь небольшой доступ к памяти, например. мандельбротов.

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

Ответ 3

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