Команда 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 и всего, что с ним связано.
