Статьи

Инструменты кода Java 9: ​​практическая сессия с использованием микробенчмаркинга Java

Невооруженным взглядом может показаться, что бенчмаркинг — это просто вопрос определения времени, которое требуется для выполнения определенных фрагментов кода. Но чаще всего это наивный подход. Обеспечение значимого ориентира, который дает точные и воспроизводимые результаты, не простая задача.

В этой статье мы хотели бы познакомить вас с проектом инструментов кода OpenJDK, и особенно с JMH. Микробенчмаркинг Java. Мы знали об этом в течение некоторого времени, но он снова привлек наше внимание, когда мы увидели, что он будет широко использоваться во время разработки Java 9.

Сравнительный тест

Так почему же простой стиль синхронизации t2-t1 не работает? Если вы не осуществляете мониторинг работающей системы, существует множество факторов, которые могут повлиять на результаты тестов и сделать их недействительными. Если вы не используете стандартизированный инструмент сравнения, такой как JMH, результаты часто будут сомнительными. И не забывайте здравый смысл. Самый важный фактор — это здравый смысл .

Обычно проблема вызвана определенной оптимизацией системы и виртуальной машины, которая может наклонить результат в одном из протестированных сценариев использования, но в другом тестовом случае не ударить. К лучшему или к худшему. Проблемы, такие как неожиданный сборщик мусора, время прогрева, устранение мертвого кода, различные оптимизации JIT-компилятора, запуск для запуска дисперсии, причуды ЦП и список можно продолжать и продолжать. Все факторы, которые не обязательно связаны с фактической вещью, которую вы должны оценивать.

Который … создал этот вариант популярной цитаты лауреата премии Тьюринга Дональда Кнута:

shipilevquote

Чтобы получить более подробное представление о том, как JMH решает эти проблемы, ознакомьтесь с этим докладом и блогом Алексея Шипилева.

Начало работы с JMH

Настройка вашего проекта для использования JMH может быть выполнена двумя способами: в виде отдельного проекта или путем добавления зависимостей как части существующего проекта с использованием maven. Инструкции доступны на официальной странице прямо здесь . Кстати, JMH также поддерживает другие языки JVM, такие как Scala, Groovy и Kotlin.

Как только вы настроили свою среду, пришло время перейти к реальному коду. JMH — это структура, основанная на аннотациях, давайте посмотрим, что это означает на практике на примере.

Пример теста: сравнение проверки URL

В этом тесте мы сравним 2 разных подхода к проверке URL с помощью Java:

1. Использование конструктора java.net.URL. Если конструктор терпит неудачу из-за того, что URL недействителен, он генерирует исключение MalformedURLException. Чтобы сделать тест более интересным, были добавлены еще два варианта, ограничив глубину трассировки стека до 6 методов и полностью отменив трассировку стека.

2. Используя регулярное выражение, довольно чудовищное регулярное выражение, мягко говоря, мухаха. Если URL не соответствует шаблону, мы заключаем, что он недействителен.

Результаты помогут нам получить окончательный ответ на этот вопрос, поэтому пришло время сделать ваши ставки. Дайте нам знать, если вы правильно поняли в разделе комментариев ниже 🙂

МОНСТР РЕГЕКС! Шаблон проверки URL. Оно живое!!!

МОНСТР РЕГЕКС! Шаблон проверки URL. Оно живое!!!

Огромное спасибо Харди Ференщику, который позволил нам поделиться его примером использования с читателями блога Takipi. Харди является главным инженером в RedHat, работает в команде Hibernate и руководит проектом Hibernate Validator .

Полный исходный код для теста доступен на GitHub . Мы рекомендуем открыть его в ближайшей вкладке и использовать оставшуюся часть этого раздела в качестве справочного руководства.

1. Настройка бенчмарка

1
2
3
4
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 1)
@Measurement(iterations = 2)
@OutputTimeUnit(TimeUnit.NANOSECONDS)

И вот объяснение того, что происходит:

@BenchmarkMode
Сначала остановимся, выбрав, какой режим тестирования мы будем использовать. JMH предлагает нам 4 различных режима: Throughput , AverageTime , SampleTime (включая процентили) и SingleShotTime (для однократного запуска метода). Любая их комбинация также вполне законна.

@ Warmup (итерации = 1)
Количество итераций разминки.

@Measurement (итерации = 2)
Количество фактических итераций измерения. В этом тесте мы проводим 2 итерации и затем усредняем результат.

@OutputTimeUnit (TimeUnit.NANOSECONDS)
В качестве единицы времени для результата используется любое значение java.util.concurrent.TimeUnit, которое имеет для вас смысл.

2. Контрольная область — начальное состояние

После того, как у нас есть настройки, нам нужно настроить начальное состояние теста. В этом случае он включает URL-адреса, которые мы будем тестировать, класс для теста регулярных выражений и класс для теста конструктора URL.

Каждый из этих классов должен быть аннотирован @State (Scope.Benchmark)

Также для списка URL обратите внимание на аннотацию @Param для подачи различных значений в тест:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
@State(Scope.Benchmark)
    public static class URLHolder {
        @Param(value = {
        // should match
        "http://foo.com/blah_blah",
        "http://142.42.1.1:8080/",
        "http://例子.测试",
        // should not match
        "http//foo/",
        "///a",
        ":// should fail"
    })
    String url;
}

3. Код теста

Теперь, когда мы настроили конфигурацию и начальное состояние, мы можем перейти к фактическому коду тестирования.

1
2
3
4
5
@Benchmark
@Fork(1)
public boolean regExp(ValidateByRegExp validator, URLHolder urlHolder) {
    return validator.isValid( urlHolder.url );
}

@Benchmark
Отмечает этот метод в качестве эталона.

@Fork (1)
Количество испытаний для запуска. Каждый прогон начинается в другой JVM. С помощью этой аннотации вы также можете указать аргументы JVM, которые вы хотите включить в тест. Таким образом, для теста трассировки ограниченного стека мы видим использование @Fork (value = 1, jvmArgs = «-XX: MaxJavaStackTraceDepth = 6») .

4. Запуск теста

Используя шаблон параметров:

1
2
3
4
5
6
public static void main(String[] args) throws Exception {
    Options opt = new OptionsBuilder()
        .include( ".*" + URLConstraintBenchmark.class.getSimpleName() + ".*" )
        .build();
        new Runner( opt ).run();
}

** Это ни в коем случае не полное руководство, просто краткое руководство, которое поможет вам ознакомиться с концепциями. Для полного набора примеров, посмотрите официальный пример кода OpenJDK прямо здесь .

Результаты

Если вам интересно, вот результаты, представленные в наносекундах. Время посмотреть, были ли ваши ставки правильными. Верхние 3 URL-адреса являются законными, а нижние 3 являются недействительными:

JMH-результаты

Мы видим, что если это действительный URL, то метод проверки регулярных выражений довольно плох. Он получил худшие результаты по всем нашим действительным URL. С другой стороны, мы видим, что если URL недействителен, таблица переворачивается, и регулярное выражение получает лучшие результаты.

На фронте конструктора URL мы не видим существенных различий в действительных URL. Каждая из вариаций дала почти одинаковые результаты, опередив регулярное выражение. Когда дело доходит до недействительных URL-адресов с добавленной в смесь MalformedURLException, есть еще одна вещь, которую следует учитывать: трассировка стека исключения. Замедление операции в отличие от чистой (но чудовищной) версии регулярных выражений.

Так какой же самый лучший выбор? Лучше всего будет использовать способ конструктора URL, если большинство ваших данных будут содержать действительные URL. Хотя в некоторых случаях может быть лучше использовать регулярные выражения, если вы предполагаете, что подавляющее большинство URL-адресов будут недействительными.

Кто использует JMH для сравнения своего кода?

Прежде всего, JMH был построен как инструмент внутреннего кода для проекта OpenJDK. Как говорит Алексей Шипилев , эксперт по производительности Java в Oracle и руководитель проекта JMH:

«JMH избавляется от собственного зуда: производительность работает на самом OpenJDK. Таким образом, у нас есть множество специфичных для функции тестов, которые оценивают производительность кода в разработке. О многих ошибках в производительности сообщается с помощью эталонного теста на основе JMH, чтобы продемонстрировать поведение, которое мы наблюдаем, и обеспечить легкий тестовый пример для проверки изменений JDK ».

Как мы уже говорили, поскольку точность эталонного теста в основном зависит от того, как он обрабатывает различные оптимизации и изменения в поведении системы, нет лучшей команды для создания такого инструмента, чем команда OpenJDK. Та же команда, которая создает JVM и включает в себя большинство из этих полезных (но трудно тестируемых) оптимизаций.

Поскольку команда, работающая над JMH, настолько близка к базовой виртуальной машине, она пользуется преимуществом перед другими инструментами и может использоваться во многих библиотеках и инструментах Java и Scala. Некоторые известные примеры включают утилиты Twitter Fingale и misc для использования в производстве , Jersey , Square Okio , различные проекты Apache, Hibernate и многие другие.

Последние мысли

Когда дело доходит до сравнительного анализа, как и многих других основных проблем Java, команда и ресурсы OpenJDK часто являются лучшим местом для поиска ответов. JMH — это простая в использовании альтернатива доморощенным (и в основном ошибочным) микробенчмаркам. Хотя это ни в коем случае не освобождает вас от здравого смысла, чтобы убедиться, что ваши тесты верны! Мы надеемся, что вы нашли этот ресурс полезным и будем продолжать изучать использование JMH для создания значимых тестов и делиться своими результатами с сообществом Java. На этой неделе мы также хотели бы поделиться некоторыми новыми достижениями, которые мы сделали в Такипи. Если вы еще не видели его в действии, вот все, что вам нужно знать, чтобы начать .