Java 8 Concurrent Hash Map получает производительность/альтернативу

У меня есть приложение с высокой пропускной способностью и низкой задержкой (3000 запросов в секунду, 100 мс на запрос), и мы активно используем Java 8 ConcurrentHashMap для выполнения поиска. Обычно эти карты обновляются одним фоновым потоком, и несколько потоков читают эти карты.

Я вижу узкое место в производительности, и при профилировании я обнаружил, что ConcurrentHashMap.get является горячей точкой и занимает большую часть времени.

В другом случае я вижу, что ConcurrentHashMap.computeIfAbsent является точкой доступа, хотя функция сопоставления имеет очень небольшую задержку, и профиль показывает, что computeIfAbsent тратит 90% времени на выполнение самого себя и очень меньше времени на выполнение функции сопоставления.

Мой вопрос, есть ли способ улучшить производительность? У меня около 80 потоков одновременно читают из CHM.


person Amm Sokun    schedule 24.11.2015    source источник
comment
Используется ли ваша hasmap больше для чтения значений, записи значений, и того, и другого? Очень сложно вам помочь, если у нас нет дополнительной информации о самом алгоритме.   -  person Guillaume F.    schedule 24.11.2015
comment
Вам следует предварительно проверить computeIfAbsent оптимистичным get, чтобы избежать штрафа за блокировку при чтении. .   -  person Ben Manes    schedule 24.11.2015
comment
@Гийом Ф. for (чтение со скоростью 3000 Rps), а запись будет примерно 100 Rps. Бен Мейнс: не могли бы вы уточнить   -  person Amm Sokun    schedule 24.11.2015
comment
Если вы говорите обновляется одним фоновым потоком, это также относится к вызовам computeIfAbsent, потому что это потенциально изменяющие действия.   -  person the8472    schedule 24.11.2015
comment
Как правило, если у вас есть общий ресурс с высокой конкуренцией, подобный этому, самый общий (хотя и неуклюжий) способ повысить производительность — это нарезать его кубиками. Есть ли способ разделить эту карту на несколько карт, где каждый поток, читающий ее, будет знать, какую из них искать?   -  person    schedule 24.11.2015
comment
В качестве очень упрощенного примера предположим, что ваши ключи являются целыми числами. В этом случае вы можете легко разделить свой CHM на два: один для четных ключей, другой для нечетных. Те читающие потоки, которые ищут ключи, могут затем быстро узнать, какой из двух CHM использовать, и в результате снижается конкуренция между потоками.   -  person    schedule 24.11.2015
comment
#the8472, потоки чтения не вызывают вызовы calculateIfAbsent, и, как указал Бен, было бы лучше выполнить оптимистическое получение перед вычислениемIfAbsent, @Ike, разве это не просто карта карты?   -  person Amm Sokun    schedule 24.11.2015
comment
@AmmSokun Не обязательно, и карта карты вряд ли поможет. Вам нужны некоторые ранее существовавшие знания о ключе (например, четный он или нечетный, первый символ строки, что-то в этом роде), чтобы определить, какую карту использовать, чтобы только локальная память для потока требовалась, чтобы определить, какой карта для поиска. Если у вас есть карта карт, то в конечном итоге вы перенесете серьезную конкуренцию на первую карту и, скорее всего, вместо этого просто окажетесь там узким местом.   -  person    schedule 24.11.2015
comment
@Ike мои ключи String   -  person Amm Sokun    schedule 24.11.2015
comment
@AmmSokun В этом случае, например, у вас может быть массив CHM (не слишком большой), основанный на хэше строки или даже на некотором модуле первого символа строки. Суть в том, что этот массив предварительно вычисляется, он никогда не изменяется (и, следовательно, может быть без блокировки/атомарности). Это может показаться незначительным отличием, но это делает каждый CHM, который вы храните в нем, более легким соперничеством. Даже быстродействующая параллельная структура обычно нуждается в атомарном CAS и платит за синхронизацию при доступе к нескольким потокам (даже для использования только для чтения).   -  person    schedule 24.11.2015
comment
@AmmSokun computeIfAbsent пессимистично блокирует перед чтением и, возможно, вычислением значения. На 32-ядерной машине это кажется быстрее в условиях конкуренции, но в остальном оптимистическое чтение намного быстрее. Это особенно верно, когда существует высокая вероятность успешного чтения, например, в кэше. Когда он переворачивается, разница менее заметна, поэтому предварительный просмотр является чистым преимуществом.   -  person Ben Manes    schedule 24.11.2015
comment
@ the8472 the8472 Это хорошая вещь, на которую стоит обратить внимание, но не так уж неестественно, чтобы get отображался здесь как точка доступа при одновременном доступе 80 потоков. Параллельные структуры данных часто используют atomics как более дешевую форму синхронизации, но стоимость синхронизации по-прежнему зависит от количества потоков, конкурирующих за одну и ту же строку кэша для атомарной переменной. В структуре непрерывного стиля, такой как CHM, обычно будет общая переменная такого рода для всей структуры. Таким образом, серьезные разногласия здесь могут увеличить стоимость этой get операции.   -  person    schedule 24.11.2015
comment
У нас была аналогичная проблема с concurrent_vector из Intel Thread Building Blocks (C++). Мы были слишком взволнованы тем фактом, что он разработан для параллелизма и сделал огромные потоки, к которым обращались потоки (как в этом случае). Это стало горячей точкой. Решение: разделить его на несколько векторов с уже существующими знаниями в каждом потоке, используемом для поиска соответствующего. Уменьшите конкуренцию --› большая скорость. Я могу немного ошибаться в архитектурных деталях, почему это помогает, но обычно это работает.   -  person    schedule 24.11.2015
comment
@Ike, мой chm не очень большой (только 5K ключей)   -  person Amm Sokun    schedule 24.11.2015
comment
@AmmSokun О, это очень мало. Но я читал о CHM - вы используете в нем параметр конструктора concurrencyLevel? Похоже, что это имеет тот же эффект, что и разделение его на более мелкие контейнеры («сегменты» в источнике).   -  person    schedule 24.11.2015
comment
В jdk 8 этот параметр не что иное, как подсказка по размеру, он больше не используется.   -  person Amm Sokun    schedule 24.11.2015
comment
@Ike, твое понимание атомарности неверно. get использует только некоторые непостоянные чтения, которые очень дешевы для x86. А именно, это обычное чтение за вычетом переупорядочения компилятора, т. е. не задействованы инструкции барьера ЦП. При 3k чтений в секунду это не должно быть проблемой. Пока они только читаются, нет когерентности кеша или чего-то подобного, потому что кеш-линии находятся в общем режиме. (Конечно, можно проверить с помощью perf или подобных инструментов)   -  person the8472    schedule 24.11.2015
comment
@BenManes: я не думаю, что calculateIfAbsent не упреждает неблокирующее получение перед попыткой вычислить отсутствующее значение. Док говорит: For example, to add a count to a ConcurrentHashMap<String,LongAdder> freqs, you can use freqs.computeIfAbsent(k -> new LongAdder()).increment(); Я тоже использую CHM для обслуживания счетчиков в том же стиле и не вижу здесь никаких проблем с производительностью.   -  person Amm Sokun    schedule 24.11.2015
comment
@BenManes: Не могли бы вы поделиться фрагментом Jdk 8 computeIfAbsent, который пессимистично получает блокировку. Я посмотрел на CHM#computeIfAbsent, но не смог многого понять   -  person Amm Sokun    schedule 24.11.2015
comment
Просто рассматриваю это с концептуальной точки зрения - не уверен, что JVM делает с volatile. Но параллельная структура такого типа обычно нуждается в каком-то барьере памяти (хотя она может быть свободной от блокировки и по-прежнему использовать атомарные инструкции - это просто перестанет делать ее свободной от ожидания).   -  person    schedule 24.11.2015
comment
brooker.co.za/blog/2012/09/10/volatile. html ‹-- это может быть интересно.   -  person    schedule 24.11.2015
comment
На самом деле вопрос говорит 3000 запросов/сек. Сколько вызовов метода CHM выполняется на запрос? Поскольку вы тратите 100 мс на запрос, вызовы CHM должны составлять либо небольшую часть, либо их должно быть больше 1 на запрос. Другими словами, вопрос предоставляет очень расплывчатые данные, вы даже не включаете вывод своего профилировщика.   -  person the8472    schedule 24.11.2015
comment
@Ike, этот пост в блоге является крайним случаем, потому что он тестирует volatile в условиях сильной конкуренции, когда несколько потоков конкурируют за одни и те же кэш-линии в течение 100% своих циклов инструкций. Неустойчивые затраты быстро амортизируются, если они чередуются с другими операциями или вещами, имеющими доступ к другим, неконкурентным.   -  person the8472    schedule 24.11.2015
comment
@AmmSokun Блок else, где переменная f означает найденную, цепочка корзин использует synchronized (f). Дуг Ли рекомендует оптимизацию предварительной проверки для машин менее чем с 32 путями, но с учетом будущего оптимизация была слишком короткой для внедрения.   -  person Ben Manes    schedule 24.11.2015
comment
@ the8472 Я вижу - хотя этот случай все еще может страдать от серьезных разногласий (маленький 5k CHM, много потоков - хотя позже я понял, что он разбит на сегменты). Хотя я с вами, что больше данных, конечно, было бы неплохо. 3k чтений в секунду звучит как ничто - я бы подумал, что что-то серьезно не так, если на запрос не так много запросов CHM.   -  person    schedule 24.11.2015
comment
@Ben ах, только что увидел ключевое слово synchronized, да, функция сопоставления вызывается внутри ключевого слова synchronized (и именно так они гарантируют, что функция сопоставления вызывается не более одного раза, хотя это должно было быть упомянуто где-то в java-doc), Pre-empt скрининг путь тогда. Спасибо!! Тоже посмотрел на Caffeine, кажется многообещающим, хотелось бы попробовать поскорее!!   -  person Amm Sokun    schedule 24.11.2015
comment
еще один интересный фрагмент кода, chm.put(key, System.currentTimeMillis(), и это делается в цикле со средним размером 100 для каждого запроса, я думаю, что лучше было бы chm.get(key).setTime(System.currentTimeMillis()). Что скажите ребята?   -  person Amm Sokun    schedule 24.11.2015
comment
Получение времени является родным вызовом и удивительно дорогим на некоторых платформах (в меньшей степени на Linux). Если возможно, я бы вытащил его за пределы цикла, чтобы избежать повторных вызовов.   -  person Ben Manes    schedule 24.11.2015
comment
внимательно прочитав javadoc для CHM в jdk8, я чувствую, что иногда javadoc может вводить в заблуждение. В моей кодовой базе у меня есть большое количество строк кода с использованием calculateIfAbsent, хотя большинство из них просто инициализируют значение, но все же очень немногие выполняют медленные вычисления, которые должны вызывать много проблем. Спасибо @Ben, это было действительно полезно.   -  person Amm Sokun    schedule 24.11.2015
comment
Профилирование не очень просто. Если какой-то профилировщик показывает, что точка доступа находится здесь или там, это не всегда означает, что она действительно существует.   -  person Tagir Valeev    schedule 25.11.2015
comment
Я согласен с @TagirValeev, много здравого смысла, знание приложений должно быть отражено в результатах профилирования. Но то, что он не прост, не исключает возможности получения пользы от профилирования.   -  person Amm Sokun    schedule 25.11.2015
comment
ConcurrentHashMap может обрабатывать около 4 миллионов операций get/put в секунду на поток и имеет задержку около 250 наносекунд. Вам нужно сделать очень много операций, чтобы занять 100 мс, что является вечностью для компьютера. Современный ЦП может за это время выполнить миллиарды инструкций.   -  person Peter Lawrey    schedule 28.11.2015
comment
@AmmSokun ты понял, что пошло не так?   -  person Nat    schedule 06.07.2017


Ответы (1)


У меня около 80 потоков одновременно читают из CHM.

Самое простое, что нужно сделать, это

  • если у вас есть процесс, связанный с процессором, не используйте больше активных потоков, чем у вас есть процессоры, иначе это только добавит накладные расходы, и если эти потоки удерживают блокировку, пока они не работают, потому что у вас слишком много потоков, это действительно не поможет.
  • увеличить количество разделов. Вам понадобится как минимум в 4 раза больше сегментов/разделов, и у вас есть потоки, обращающиеся к одной карте. Однако вы получите странное поведение в CHM, если вы получите доступ к нему с более чем 40 потоками из-за того, как работает когерентность кэша. Я предлагаю использовать более эффективную структуру данных для более высокой степени параллелизма. В Java 8 concurrencyLevel является подсказкой, но это лучше, чем оставить размер инициализации по умолчанию равным 16.
  • не проводите так много времени в CHM. Найдите способ выполнять полезную работу, не затрагивая общий ресурс, и ваши потоки будут работать намного эффективнее.

Если у вас есть какие-либо задержки, которые вы можете увидеть в системе с низкой задержкой, у вас есть проблема, ИМХО.

person Peter Lawrey    schedule 28.11.2015
comment
` вы получите странное поведение в CHM, если вы получите доступ к нему с более чем 40 потоками ` Можете ли вы уточнить этот вопрос ? Какое было бы поведение? Ведь CHM предназначен для одновременного доступа, не так ли? Также я не вижу никаких проблем с огромным количеством потоков, читающих, если только не много писателей. - person Svetlin Zarev; 30.11.2015
comment
@SvetlinZarev хорошая мысль, чтение не должно быть проблемой. В системах с большим количеством процессоров я наблюдал проблемы масштабируемости с CHM, когда у вас более 40 активных потоков, использующих один и тот же CHM. Скорее всего, это аппаратная, а не программная проблема, поскольку машина использовала несколько сокетов. Когда у нас будет 40 потоков на одном сокете, я подозреваю, что проблема исчезнет. - person Peter Lawrey; 30.11.2015
comment
Спасибо @Питер!! QQ: Еще одна вещь, которую я обнаружил, заключалась в том, что использование CHM с ключом, поскольку java.util.TimeZone get занимает больше времени, чем использование CHM со строкой в ​​качестве ключа. Есть мысли по этому поводу? Это связано с столкновениями? - person Amm Sokun; 01.12.2015
comment
@AmmSokun Хэш-код для SimpleTimeZone довольно плохой, но он также может быть плохим, поскольку он не сопоставим. HashMap в Java 8 использует дерево для строковых ключей, однако связанный список для коллизий для такого класса, как TimeZone. - person Peter Lawrey; 01.12.2015
comment
А как насчет класса ZoneInfo? TimeZone.getInstance() возвращает объект класса ZoneInfo. - person Amm Sokun; 01.12.2015
comment
@AmmSokun ZoneInfo имеет более эффективный/случайный hashCode(). Если вы видите проблемы, возможно, у вас много коллизий. Вы можете уменьшить коэффициент нагрузки, чтобы увидеть, поможет ли это. - person Peter Lawrey; 01.12.2015