Benchmark java что это
Замеры производительности на Java с JMH
Jul 17, 2020 · 6 min read
Практически каждому разработчику известна фраза, сказанная Дональдом Кнутом в 1974 году: “Преждевременная оптимизация — корень всех зол”. Но откуда мы должны узнать, что именно стоит оптимизировать?
С тех пор вычислительные мощности многократно возросли. Но настрой на то, чтобы сосредоточиться на реальных проблемах, стоящих оптимизации, по-прежнему сохраняется. Понимание различных видов задержек и того, как найти по-настоящему узкие места, а не только те, которые кажутся таковыми, — вот что можно назвать ключом к хорошей оценке производительности.
Принцип Парето
Аксиома управления бизнесом, названная в честь итальянского экономиста Вильфредо Парето, гласит:
80% продаж приходится на 20% клиентов.
То же распредел е ние применимо ко многим областям деятельности. В информатике мы можем применить этот принцип к нашим усилиям по оптимизации. 80% фактической работы и времени приходится на 20% кода.
Среди этих 80% могут отыскаться легкодоступные плоды для нашей деятельности по оптимизации. Но мы должны сосредоточиться на более трудных 20%, если хотим добиться реального эффекта.
Чтобы выявить узкие места и код, который стоит оптимизировать, нам нужны правильные бенчмарки.
Различные виды задержки
Компьютеры — очень сложные системы. Чем дальше мы удаляемся от ядра, процессора, тем медленнее всё становится. И множество разных частей задействуется еще до того, как наш код действительно доберется до кремния.
Снижение скорости также происходит нелинейно. Как разработчики, мы должны знать, какие факторы в действительности отличают различные виды задержки, чтобы понимать, какие части стоит оптимизировать.
Разница в числах воспринимается не очень легко, поэтому представьте, что одна ссылка на кэш L1 займет 1 секунду.
На получение доступа к данным в нашей основной памяти теперь потребуется примерно столько же времени, сколько на чистку зубов.
Чтение 1 МБ из основной памяти заняло бы 50 минут, а на чтение такого же объема с SSD-накопителя ушло бы больше половины дня.
Один пакет туда и обратно между Амстердамом и Сан-Франциско путешествовал бы почти пять лет!
Этот пример доказывает, что нам нужно оптимизировать много циклов процессора, прежде чем это действительно начнет иметь значение, если сравнивать с другими видами задержек. Сохранять в памяти несколько переборов данных — это отлично, но кэшировать некоторые данные вместо того, чтобы каждый раз получать их из базы данных, может оказаться предпочтительнее в плане оптимизации.
Оптимизация компилятора и среды выполнения
Одним из самых больших врагов оценки производительности являются компиляторы и среды выполнения.
Все компиляторы в той или иной степени пытаются оптимизировать наш код. Они изменяют фактический исходный код перед компиляцией в инструкции машинного кода. Среды выполнения и виртуальные машины еще хуже. Работая с промежуточным языком, таким как байт-код или CIL, они могут оптимизировать код в самый последний момент.
Это означает, что фактическая производительность может быть не постоянной и подверженной изменениям, а все из-за того, что среда выполнения или виртуальная машина лучше понимают ваш код и оптимизируют его глубже.
В результате мы не можем проверить код, просто запустив его несколько раз в цикле, а затем измерив с помощью секундомера время после вызова метода.
Java Microbenchmark Harness
Самый простой способ по-настоящему проверить свой код — это Java Microbenchmark Harness (JMH). Он помогает оценить фактическую производительность, принимая во внимание прогрев JVM и оптимизацию кода, которые могут сделать результат неясным.
Знакомство с Harness
JMH стал де-факто стандартом для тестов производительности и был включен в JDK 12. До этой версии зависимости нужно добавлять вручную:
Мы можем запустить бенчмарк с помощью IDE или даже предпочитаемой системы сборки:
Создание теста производительности (бенчмарка)
Это так же просто, как создать модульный тест: создайте новый файл, добавьте туда метод benchmark с аннотацией @Benchmark и основную оболочку для его запуска:
Конечный результат выглядит примерно так:
Типы бенчмарков
Доступны следующие четыре типа тестов производительности:
Работа с JVM
Сколько времени?
Мы можем указать, за какую единицу времени нужно выводить результаты, добавив аннотацию @OutputTimeUnit( ) :
Управление состояниями
Если мы используем класс состояния в методе бенчмарка, JMH установит параметры соответственно и запустит тест для каждого значения:
Лучшие практики
Чтобы от замеров производительности была польза, они должны уметь обойти оптимизацию JVM, или мы просто проверим, насколько хороша JVM, а не наш код.
Мертвый код
JVM способна определить, присутствует ли у вас мертвый код, и удалить его:
Переменная result ни разу не используется в коде, поэтому это по факту — мертвый код, и все три строки внутри бенчмарка будут удалены.
Есть два варианта, чтобы заставить JVM не убирать мертвый код:
Оптимизация постоянных величин
Даже если мы будем возвращать result или использовать Blackhole, чтобы предотвратить удаление мертвого кода, JVM может оптимизировать значения постоянных. Это сводит наш код к чему-то вроде этого:
Предоставление класса состояния не дает JVM “оптимизировать” (выкидывать) постоянные величины:
Небольшие модули
Измерение производительности во многом напоминает модульное тестирование. Не стоит проводить тесты или замеры над большими элементами кода. Чем меньше элементы кода, тем меньше возможные побочные эффекты. Нам нужно свести к минимуму все, что может загрязнить результаты замеров производительности.
(Почти) Продакшен
Каждый раз, когда вы видите результаты тестов производительности, выполненных на машине разработчика, такой как MacBook Pro, отнеситесь к ним с недоверием. Машины разработчиков ведут себя по-другому в сравнении с продакшен-окружениями, и это зависит от множества параметров (например, настройки виртуальной машины, процессор, память, операционная система, системные настройки и т. д.).
Например, моя среда для разработки на Java состоит из нескольких контейнеров Docker на одной машине (Eclipse, MySQL, MongoDB, RabbitMQ), а также некоторых других контейнеров (ELK-Stack, Postgres, Killbill, MariaDB). Все они делят одни и те же 32 ГБ оперативной памяти и 8 потоков процессора. Продакшен, в свою очередь, распределяется между несколькими хостами, с меньшим количеством контейнеров и удвоением потоков оперативной памяти и процессора, а также конфигурацией SSD RAID 1.
Результаты замеров производительности будут не очень репрезентативными, если мы достигнем пределов нашего аппаратного обеспечения. Замеры должны отображать реальную производительность кода, а не “почти идентичную” настройку среды разработки.
Запуск бенчмарков локально — хорошая отправная точка, но она не обязательно будет таким же хорошим отражением реального мира, особенно в плане граничных случаев.
Заключение
Хорошие (микро-)бенчмарки — это сложно. Почти всё в процессе продвижения от нашего исходного кода до запуска исполнения на кремнии работает против точных измерений производительности. Но с помощью JMH мы получаем достаточно контроля, чтобы добиться надежных результатов.
Корень оптимизации заключается в том, чтобы перестать беспокоиться о ненужных вещах. Действительно ли результаты ваших замеров производительности применимы к выполнению кода в реальности? Взгляните на общую картину и сосредоточьтесь на реальных проблемах, таких как оптимизация доступа к данным, алгоритмов и структур данных.
В этой статье мы только поверхностно коснулись темы замеров производительности с помощью JMH. Он послужит мощным дополнением к вашему набору инструментов.
Измеряем скорость кода Java правильно (используя JMH)
Это вводная статья про то, как следует делать тесты производительности на JVM языках (java, kotlin, scala и тд.). Она полезна для случая, когда требуется в цифрах показать изменение производительности от использования определенного алгоритма.
Все примеры приведены на языке kotlin и для системы сборки gradle. Исходный код проекта доступен на github.
Подготовка
В первую очередь остановимся на основной части наших замеров — использовании JMH. Java Microbenchmark Harness — набор библиотек для тестирования производительности небольших функций (то есть тех, где пауза GC увеличивает время работы в разы).
Перед запуском теста JMH перекомпилирует код, так как:
JMH Gradle Plugin
Как понятно из описания выше, для тестирования производительности кода недостаточно просто добавить необходимые библиотеки в classpath и запустить тесты в стиле JUnit. А потому, если мы хотим делать дело, а не разбираться в особенности написания билд скриптов, нам не обойтись без плагина к maven/gradle. Для новых проектов преимущество остается за gradle, потому выбираем его.
Для JMH есть полуофициальный плагин для gradle — jmh-gradle-plugin. Добавляем его в проект:
Плагин автоматом создаст новый source set (это «набор файлов и ресурсов, которые должны компилироваться и запускаться вместе», прочитать можно или статью на хабре за авторством svartalfar, или же в официальной документации gradle). jmh source set автоматически ссылается на main, то есть получаем короткий алгоритм работы:
Получаем следующую иерархию каталогов:
Или как это выглядит в IntelliJ Idea:
С плагином есть пара интересных особенностей на Windows:
Тестирование
В качестве примера я возьму вопрос (ранее заданный на kotlin discussions), который мучал меня ранее — зачем в конструкции use используется inline метод?
В kotlin есть полный аналог, который имеет немного другой синтаксис:
Итак, необходимо сделать два метода:
Код с JMH аттрибутами, который будет запускать разные функции:
Dead Code Elimination
Java Compiler & JIT довольно умные и имеют ряд оптимизаций, как в compile time, так и в runtime. Метод ниже, например, вполне может свернуться в одну строку (как для kotlin, так и для java):
И в итоге мы будем тестировать метод:
Однако результат ведь никак не используется, потому компиляторы (byte code + JIT) в итоге вообще выкинут метод, так как он в принципе не нужен.
Чтобы избежать этого, в JMH существует специальный класс «черная дыра» — Blackhole. В нем есть методы, которые с одной стороны не делают ничего, а с другой стороны — не дают JIT выкинуть ветку с результатом.
А для того, чтобы javac не пытался сложить-таки a и b в процессе компиляции, нам требуется определить объект state, в котором будут храниться наши значения. В итоге в самом тесте мы будем использовать уже подготовленный объект (то есть не тратим время на его создание и не даем компилятору применить оптимизации).
В итоге для грамотного тестирования нашей функции требуется её написать вот в таком виде:
Здесь мы взяли a и b из некоторого state, что помешает компилятору сразу посчитать выражение. А результат мы отправили в черную дыру, что помешает JIT выкинуть последнюю часть функции.
Возвращаясь к моей функции:
Результат теста
Или, если сократить таблицу:
В результате есть две самые важные метрики:
Заключение
При разработке ПО есть два довольно частых способа сравнения производительности:
Однако, как знает любой технически подкованный специалист, оба этих варианта зачастую приводят к ошибочным суждениям, тормозам в приложениях и пр. Я надеюсь, что эта статья поможет вам делать хорошее и быстрое ПО.
Учебное пособие по тестированию Java JMH
В Java мы можем использовать инфраструктуру JMH (Java Microbenchmark Harness) для измерения производительности функции.
1. JMH
Чтобы использовать JHM, нам нужно объявить jmh-core а также jmh-generator-annprocess (Аннотации JMH)
2. JMH — Mode.AverageTime
2.1 JMH Mode.AverageTime Пример для измерения производительности различных методов зацикливания List содержащий 10 миллионов строк.
2.2 В приведенном выше коде JMH создаст 2 вилки, каждая из которых содержит 5 итераций разминки (разогрев JVM, результат игнорируется) и 5 итераций измерения (для расчета), например:
2.3 Итерация прогрева и итерация измерения настраиваются:
2.4 Мы даже можем разогреть всю вилку, прежде чем запустим настоящую вилку для измерения.
Есть два способа запустить тест JMH, использовать Maven или запустить его непосредственно через класс JMH Runner.
3.1 Maven, упакуйте его как JAR и запустите через org.openjdk.jmh.Main учебный класс.
Вы можете запустить тест через класс JMH Runner напрямую.
5. Результат
5.1 Просмотрите результат, чтобы зациклить List содержащий 10 миллионов объектов String, классический while loop самая быстрая петля. Однако разница не так существенна.
5.2 Полная информация, хорошая для справки.
Заметка
Надеюсь, что это руководство даст вам краткое руководство по использованию эталона JMH, для более подробных примеров JMH, пожалуйста, посетите эту официальную ссылку на образец JMH. Примечание
Как насчет прямого цикла против обратного цикла? Какой из них быстрее? Посетите этот тест JMH
Микробенчмаркирование с Java
Узнайте больше о JMH, Java Microbenchmark Harness.
1. Введение
Эта быстрая статья посвящена JMH (Harness Java Microbenchmark). Во-первых, мы знакомимся с API и узнаем его основы. Тогда мы хотели бы видеть несколько лучших практик, которые мы должны рассмотреть при написании микробенчмарков.
Проще говоря, JMH заботится о таких вещах, как JVM разминки и код-оптимизации пути, что делает бенчмаркинг как можно проще.
2. Начало работы
Чтобы начать работу, мы можем продолжать работать с Java 8 и просто определить зависимости:
Последние версии JMH Основные и Процессор аннотации JMH можно найти в Maven Central.
Далее, создать простой ориентир, используя @Benchmark аннотация (в любом общественном классе):
Затем мы добавляем основной класс, который начинает процесс бенчмаркинга:
Теперь работает БенчмаркРаннер будет выполнять наши, возможно, несколько бесполезный ориентир. После завершения выполнения представлена сводная таблица:
3. Типы контрольных показателей
Полученная таблица будет иметь среднюю метрику времени (вместо пропускной способности):
4. Настройка разминки и исполнения
Используя @Fork аннотация, мы можем настроить, как происходит выполнение эталона: значение параметр контролирует, сколько раз будет выполнен эталон, и разминка параметр контролирует, сколько раз эталон высохнет до сбора результатов, например:
Это поручает JMH запустить две вилки разминки и отказаться от результатов, прежде чем перейти на бенчмаркинг в режиме реального времени.
Кроме того, @Warmup аннотация может быть использована для управления числом итераций разминки. Например, @Warmup (итерации) говорит JMH, что пять итераций разминки будет достаточно, в отличие от по умолчанию 20.
5. Государство
Мы можем исследовать влияние производительности с помощью Государственные объект:
Наш метод эталона будет выглядеть так:
Здесь поле итерации будут заселены соответствующими значениями от @Param аннотация JMH, когда она передается методу бенчмарка. @Setup аннотированный метод вызывается перед каждым вызовом эталона и создает новый Ашер обеспечение изоляции.
Когда выполнение будет завершено, мы получим результат, аналогичный приведению ниже:
6. Ликвидация мертвого кода
Чтобы сделать ситуацию более конкретной, рассмотрим пример:
Мы ожидаем, что распределение объектов стоит дороже, чем ничего не делать вообще. Однако, если мы забудем тесты:
Чтобы предотвратить эту оптимизацию, мы должны как-то обмануть компилятор и заставить его думать, что код используется каким-то другим компонентом. Одним из способов достижения этой цели является просто вернуть созданный объект:
Кроме того, мы можем позволить Блэкхол потреблять его:
7. Постоянное складывание
Рассмотрим еще один пример:
Расчеты, основанные на константах, могут возвращать точно такой же выход, независимо от количества выполнений. Таким образом, существует довольно хороший шанс, что компилятор JIT заменит вызов функции logarithm своим результатом:
Чтобы предотвратить постоянное складывание, мы можем инкапсулировать постоянное состояние внутри объекта состояния:
Если мы забудем эти тесты друг против друга:
8. Заключение
В этом учебнике основное внимание было сосредоточено на микро-бенчмаркинговой упряжке Java.
Замеры производительности на Java с JMH
Практически каждому разработчику известна фраза, сказанная Дональдом Кнутом в 1974 году: “Преждевременная оптимизация — корень всех зол”. Но откуда мы должны узнать, что именно стоит оптимизировать?
С тех пор вычислительные мощности многократно возросли. Но настрой на то, чтобы сосредоточиться на реальных проблемах, стоящих оптимизации, по-прежнему сохраняется. Понимание различных видов задержек и того, как найти по-настоящему узкие места, а не только те, которые кажутся таковыми, — вот что можно назвать ключом к хорошей оценке производительности.
Принцип Парето
Аксиома управления бизнесом, названная в честь итальянского экономиста Вильфредо Парето, гласит:
80% продаж приходится на 20% клиентов.
То же распределение применимо ко многим областям деятельности. В информатике мы можем применить этот принцип к нашим усилиям по оптимизации. 80% фактической работы и времени приходится на 20% кода.
Среди этих 80% могут отыскаться легкодоступные плоды для нашей деятельности по оптимизации. Но мы должны сосредоточиться на более трудных 20%, если хотим добиться реального эффекта.
Чтобы выявить узкие места и код, который стоит оптимизировать, нам нужны правильные бенчмарки.
Различные виды задержки
Компьютеры — очень сложные системы. Чем дальше мы удаляемся от ядра, процессора, тем медленнее всё становится. И множество разных частей задействуется еще до того, как наш код действительно доберется до кремния.
Снижение скорости также происходит нелинейно. Как разработчики, мы должны знать, какие факторы в действительности отличают различные виды задержки, чтобы понимать, какие части стоит оптимизировать.
Разница в числах воспринимается не очень легко, поэтому представьте, что одна ссылка на кэш L1 займет 1 секунду.
На получение доступа к данным в нашей основной памяти теперь потребуется примерно столько же времени, сколько на чистку зубов.
Чтение 1 МБ из основной памяти заняло бы 50 минут, а на чтение такого же объема с SSD-накопителя ушло бы больше половины дня.
Один пакет туда и обратно между Амстердамом и Сан-Франциско путешествовал бы почти пять лет!
Этот пример доказывает, что нам нужно оптимизировать много циклов процессора, прежде чем это действительно начнет иметь значение, если сравнивать с другими видами задержек. Сохранять в памяти несколько переборов данных — это отлично, но кэшировать некоторые данные вместо того, чтобы каждый раз получать их из базы данных, может оказаться предпочтительнее в плане оптимизации.
Оптимизация компилятора и среды выполнения
Одним из самых больших врагов оценки производительности являются компиляторы и среды выполнения.
Все компиляторы в той или иной степени пытаются оптимизировать наш код. Они изменяют фактический исходный код перед компиляцией в инструкции машинного кода. Среды выполнения и виртуальные машины еще хуже. Работая с промежуточным языком, таким как байт-код или CIL, они могут оптимизировать код в самый последний момент.
Это означает, что фактическая производительность может быть не постоянной и подверженной изменениям, а все из-за того, что среда выполнения или виртуальная машина лучше понимают ваш код и оптимизируют его глубже.
В результате мы не можем проверить код, просто запустив его несколько раз в цикле, а затем измерив с помощью секундомера время после вызова метода.
Java Microbenchmark Harness
Самый простой способ по-настоящему проверить свой код — это Java Microbenchmark Harness (JMH). Он помогает оценить фактическую производительность, принимая во внимание прогрев JVM и оптимизацию кода, которые могут сделать результат неясным.
Знакомство с Harness
JMH стал де-факто стандартом для тестов производительности и был включен в JDK 12. До этой версии зависимости нужно добавлять вручную:
Мы можем запустить бенчмарк с помощью IDE или даже предпочитаемой системы сборки:
Создание теста производительности (бенчмарка)
Это так же просто, как создать модульный тест: создайте новый файл, добавьте туда метод benchmark с аннотацией @Benchmark и основную оболочку для его запуска:
Конечный результат выглядит примерно так:
Типы бенчмарков
Доступны следующие четыре типа тестов производительности:
Работа с JVM
Сколько времени?
Мы можем указать, за какую единицу времени нужно выводить результаты, добавив аннотацию @OutputTimeUnit( ) :
Управление состояниями
Если мы используем класс состояния в методе бенчмарка, JMH установит параметры соответственно и запустит тест для каждого значения:
Лучшие практики
Чтобы от замеров производительности была польза, они должны уметь обойти оптимизацию JVM, или мы просто проверим, насколько хороша JVM, а не наш код.
Мертвый код
JVM способна определить, присутствует ли у вас мертвый код, и удалить его:
Переменная result ни разу не используется в коде, поэтому это по факту — мертвый код, и все три строки внутри бенчмарка будут удалены.
Есть два варианта, чтобы заставить JVM не убирать мертвый код:
Оптимизация постоянных величин
Даже если мы будем возвращать result или использовать Blackhole, чтобы предотвратить удаление мертвого кода, JVM может оптимизировать значения постоянных. Это сводит наш код к чему-то вроде этого:
Предоставление класса состояния не дает JVM “оптимизировать” (выкидывать) постоянные величины:
Небольшие модули
Измерение производительности во многом напоминает модульное тестирование. Не стоит проводить тесты или замеры над большими элементами кода. Чем меньше элементы кода, тем меньше возможные побочные эффекты. Нам нужно свести к минимуму все, что может загрязнить результаты замеров производительности.
(Почти) Продакшен
Каждый раз, когда вы видите результаты тестов производительности, выполненных на машине разработчика, такой как MacBook Pro, отнеситесь к ним с недоверием. Машины разработчиков ведут себя по-другому в сравнении с продакшен-окружениями, и это зависит от множества параметров (например, настройки виртуальной машины, процессор, память, операционная система, системные настройки и т. д.).
Например, моя среда для разработки на Java состоит из нескольких контейнеров Docker на одной машине (Eclipse, MySQL, MongoDB, RabbitMQ), а также некоторых других контейнеров (ELK-Stack, Postgres, Killbill, MariaDB). Все они делят одни и те же 32 ГБ оперативной памяти и 8 потоков процессора. Продакшен, в свою очередь, распределяется между несколькими хостами, с меньшим количеством контейнеров и удвоением потоков оперативной памяти и процессора, а также конфигурацией SSD RAID 1.
Результаты замеров производительности будут не очень репрезентативными, если мы достигнем пределов нашего аппаратного обеспечения. Замеры должны отображать реальную производительность кода, а не “почти идентичную” настройку среды разработки.
Запуск бенчмарков локально — хорошая отправная точка, но она не обязательно будет таким же хорошим отражением реального мира, особенно в плане граничных случаев.
Заключение
Хорошие (микро-)бенчмарки — это сложно. Почти всё в процессе продвижения от нашего исходного кода до запуска исполнения на кремнии работает против точных измерений производительности. Но с помощью JMH мы получаем достаточно контроля, чтобы добиться надежных результатов.
Корень оптимизации заключается в том, чтобы перестать беспокоиться о ненужных вещах. Действительно ли результаты ваших замеров производительности применимы к выполнению кода в реальности? Взгляните на общую картину и сосредоточьтесь на реальных проблемах, таких как оптимизация доступа к данным, алгоритмов и структур данных.
В этой статье мы только поверхностно коснулись темы замеров производительности с помощью JMH. Он послужит мощным дополнением к вашему набору инструментов.