Команда Spring АйО перевела статью, в которой приведено несколько правил, которые следует учитывать при написании микробенчмарков для HotSpot JVM.
Дисклеймер от команды Spring АйО
Данная статья написана до официального релиза JMH, и говорит о том, как раньше люди писали бенчмарки
Вот несколько правил (в порядке приоритета), которые следует учитывать при написании микробенчмарков для HotSpot JVM:
Правило 0:
Прочтите авторитетную статью о JVM и микробенчмарках. Хороший пример — статья Брайана Гетца, 2005 года.
Не ожидайте слишком многого от микробенчмарков: они измеряют лишь узкий спектр характеристик производительности JVM.
Правило 1:
Всегда включайте фазу прогрева, в которой тестовая нагрузка прогоняется полностью, достаточную для того, чтобы запустить все инициализации и JIT-компиляции до начала фазы замера. (В фазе прогрева допустимо меньшее количество итераций; На практике, как правило, — несколько десятков тысяч итераций внутреннего цикла.)
Правило 2:
Запускайте с флагами -XX:+PrintCompilation
, -verbose:gc
и т.п., чтобы удостовериться, что JIT компилятор уже закончил компиляцию, и что другие подсистемы JVM не выполняют какую-то неожиданную работу (например, сборку мусора) во время фазы замера.
Правило 2.1:
Выводите сообщения в начале и в конце фаз прогрева и замера — так вы сможете убедиться, что в фазе замера отсутствует вывод от PrintCompilation и других флагов.
Правило 3:
Различайте режимы -client
и -server
, а также OSR (On-Stack Replacement) и обычную компиляцию. Флаг -XX:+PrintCompilation
отображает OSR-компиляции с @, например: Trouble$1::run @ 2 (41 bytes)
.
Для максимальной производительности предпочтительнее флаг -server
и обычная компиляция. Также учитывайте эффект флага -XX:+TieredCompilation
, который смешивает клиентский и серверный режимы.
Комментарий от команды Spring АйО
Флаги -client
и -server
являются устаревшими. Ранее различали клиентский и серверный моды JIT компилятора. Это было довольно давно, и сделано было с целью выбрать режим компиляции, где клиентский мод применял довольно базовые оптимизации, но при этом довольно быстро проводил компиляцию. Такое подходит для короткоживущих приложений. Серверный же мод предназначался долгоживущих "серверных" приложений.
Серверный мод применял очень агрессивные оптимизации, и скомпилированный таким JIT-ом код мог оказаться существенно быстрее, чем аналогичный с использованием клиентского JIT компилятора.
Уже довольно давно в Java используется так называемся "tired compilation", где сначала идёт компиляция C1 компилятором (aka клиентская), а потом, если имеет смысл, идёт компиляция C2 (aka серверная). То есть на данный момент JVM сама решит, какой мод для какого кода подойдёт лучше всего.
Правило 4:
Учитывайте эффекты инициализации.
Не производите вывод в консоль впервые во время фазы замера — это приведёт к загрузке и инициализации классов.
Не загружайте новые классы за пределами фазы прогрева (или финального отчёта), если только вы специально не тестируете загрузку классов.
Правило 5:
Избегайте деоптимизации и перекомпиляции. Не заходите в новую ветку выполнения кода впервые в фазе замера — компилятор может отменить предыдущую оптимизацию и перекомпилировать код.
Флаг PrintCompilation
помогает выявлять такие ситуации.
Комментарий от команды Spring АйО
Довольно большая часть агрессивных оптимизаций C2 JIT-а связана с наличием определённых предположений. Например, методfoo(boolean b)
был вызван уже 10 000+ раз, и почти каждый раз в него попадал true
. Это позволяет JIT-у просто выбросить определённые куски в рамках метода, убрать ненужный branch prediction и т.д, и скомпилировать метод foo
для b = true
(т.к. это же самый частый юз кейс).
А теперь представьте, что вдруг начали приходить false
каждый 3-ый раз. Это уже довольно часто. И тут JIT понимает, что он природа нагрузки изменилась, а интерпретировать метод медленно. Поэтому JIT принимает решение депотимизировать, т.е. убрать скомпилированный С2 код, который предполагал, что в 99% случаев там true
- этот уже себя не оправдывает.
JIT убирает этот оптимизированный С2 код из code cache, и начинает заново выявлять паттерны в природе нагрузки, чтобы потом заново поместить в С2 код, но уже другой, под новую природу нагрузки.
Правило 6:
Используйте подходящие инструменты для «чтения мыслей» компилятора и будьте готовы к неожиданным результатам.
Перед тем как делать выводы о производительности, проанализируйте сгенерированные JIT-ом assembly инструкции.
Правило 7:
Минимизируйте «шум» в измерениях.
Запускайте бенчмарк на «тихой» системе, повторяйте тест несколько раз и отбрасывайте выбросы.
Используйте -Xbatch
, чтобы компилятор не работал параллельно с приложением, и рассмотрите -XX:CICompilerCount=1
для того, чтобы ограничить количество потоков, используемых при компиляции байткода.

Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.