Автор оригинала: Nguyen Nam Thai.
1. Обзор
В Java исключения обычно считаются дорогостоящими и не должны использоваться для управления потоком. Этот учебник докажет, что это восприятие является правильным, и точно укажет, что вызывает проблему с производительностью.
2. Настройка Среды
Прежде чем писать код для оценки затрат на производительность, нам необходимо создать среду бенчмаркинга.
2.1. Жгут проводов Java Microbenchmark
Измерение накладных расходов на исключение не так просто, как выполнение метода в простом цикле и учет общего времени.
Причина в том, что компилятор точно в срок может помешать и оптимизировать код. Такая оптимизация может привести к тому, что код будет работать лучше, чем на самом деле в производственной среде. Другими словами, это может привести к ложноположительным результатам.
Чтобы создать управляемую среду, которая может смягчить оптимизацию JVM, мы будем использовать Java Microbenchmark Harness или сокращенно JMH.
В следующих подразделах будет рассмотрена настройка среды бенчмаркинга, не вдаваясь в подробности JMH. Для получения дополнительной информации об этом инструменте, пожалуйста, ознакомьтесь с нашим учебником по микробенчмаркингу с Java.
2.2. Получение артефактов JMH
Чтобы получить артефакты JMH, добавьте эти две зависимости в POM:
org.openjdk.jmh jmh-core 1.28 org.openjdk.jmh jmh-generator-annprocess 1.28
Пожалуйста, обратитесь к Maven Central для получения последних версий JMH Core и JMH Annotation Processor .
2.3. Эталонный класс
Нам понадобится класс для проведения тестов:
@Fork(1) @Warmup(iterations = 2) @Measurement(iterations = 10) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.MILLISECONDS) public class ExceptionBenchmark { private static final int LIMIT = 10_000; // benchmarks go here }
Давайте рассмотрим аннотации JMH, показанные выше:
- @Fork : Указание количества раз, когда JMH должен порождать новый процесс для запуска тестов. Мы устанавливаем его значение равным 1, чтобы генерировать только один процесс, избегая слишком долгого ожидания, чтобы увидеть результат
- @Warmup : Перенос параметров прогрева. Элемент iterations , равный 2, означает, что первые два прогона игнорируются при вычислении результата
- @Измерение : Перенос параметров измерения. Значение итерации 10 указывает на то, что JMH выполнит каждый метод 10 раз
- @BenchmarkMode : Именно так JHM должен собирать результаты выполнения. Значение Среднее время требует, чтобы OMH подсчитал среднее время, необходимое методу для завершения его операций
- @OutputTimeUnit : Указывает единицу времени вывода, которая в данном случае является миллисекундой
Кроме того, внутри тела класса есть статическое поле, а именно LIMIT . Это число итераций в теле каждого метода.
2.4. Выполнение контрольных показателей
Для выполнения тестов нам нужен метод main :
public class MappingFrameworksPerformance { public static void main(String[] args) throws Exception { org.openjdk.jmh.Main.main(args); } }
Мы можем упаковать проект в файл JAR и запустить его в командной строке. Это, конечно, приведет к пустому результату, поскольку мы не добавили никакого метода бенчмаркинга.
Для удобства мы можем добавить maven-jar-плагин в POM. Этот плагин позволяет нам выполнять метод main внутри IDE:
org.apache.maven.plugins maven-jar-plugin 3.2.0 com.baeldung.performancetests.MappingFrameworksPerformance
Последнюю версию maven-jar-plugin можно найти здесь .
3. Измерение Производительности
Пришло время разработать некоторые методы бенчмаркинга для измерения производительности. Каждый из этих методов должен содержать аннотацию @Benchmark .
3.1. Метод, Возвращающийся Нормально
Давайте начнем с метода, возвращающегося нормально; то есть с метода, который не вызывает исключения:
@Benchmark public void doNotThrowException(Blackhole blackhole) { for (int i = 0; i < LIMIT; i++) { blackhole.consume(new Object()); } }
Параметр black hole ссылается на экземпляр Black hole . Это класс JMH, который помогает предотвратить устранение мертвого кода, оптимизацию, которую может выполнить компилятор just-in-time.
Бенчмарк в этом случае не создает никаких исключений. На самом деле, мы будем использовать его в качестве ссылки для оценки производительности тех, кто создает исключения.
Выполнение метода main даст нам отчет:
Benchmark Mode Cnt Score Error Units ExceptionBenchmark.doNotThrowException avgt 10 0.049 ± 0.006 ms/op
В этом результате нет ничего особенного. Среднее время выполнения бенчмарка составляет 0,049 миллисекунды, что само по себе довольно бессмысленно.
3.2. Создание и выбрасывание исключения
Вот еще один тест, который выдает и улавливает исключения:
@Benchmark public void throwAndCatchException(Blackhole blackhole) { for (int i = 0; i < LIMIT; i++) { try { throw new Exception(); } catch (Exception e) { blackhole.consume(e); } } }
Давайте посмотрим на результат:
Benchmark Mode Cnt Score Error Units ExceptionBenchmark.doNotThrowException avgt 10 0.048 ± 0.003 ms/op ExceptionBenchmark.throwAndCatchException avgt 10 17.942 ± 0.846 ms/op
Небольшое изменение во времени выполнения метода doesnotthrowexception не имеет значения. Это просто колебания в состоянии базовой ОС и JVM. Ключевой вывод заключается в том, что выбрасывание исключения заставляет метод работать в сотни раз медленнее.
В следующих нескольких подразделах будет выяснено, что именно приводит к такой разительной разнице.
3.3. Создание Исключения Без Его выбрасывания
Вместо того, чтобы создавать, выбрасывать и перехватывать исключение, мы просто создадим его:
@Benchmark public void createExceptionWithoutThrowingIt(Blackhole blackhole) { for (int i = 0; i < LIMIT; i++) { blackhole.consume(new Exception()); } }
Теперь давайте выполним три контрольных показателя, которые мы объявили:
Benchmark Mode Cnt Score Error Units ExceptionBenchmark.createExceptionWithoutThrowingIt avgt 10 17.601 ± 3.152 ms/op ExceptionBenchmark.doNotThrowException avgt 10 0.054 ± 0.014 ms/op ExceptionBenchmark.throwAndCatchException avgt 10 17.174 ± 0.474 ms/op
Результат может оказаться неожиданным: время выполнения первого и третьего методов почти одинаково, в то время как время выполнения второго существенно меньше.
На данный момент ясно, что заявления throw и catch сами по себе довольно дешевы. С другой стороны, создание исключений приводит к высоким накладным расходам.
3.4. Создание исключения Без добавления трассировки стека
Давайте выясним, почему создание исключения намного дороже, чем создание обычного объекта:
@Benchmark @Fork(value = 1, jvmArgs = "-XX:-StackTraceInThrowable") public void throwExceptionWithoutAddingStackTrace(Blackhole blackhole) { for (int i = 0; i < LIMIT; i++) { try { throw new Exception(); } catch (Exception e) { blackhole.consume(e); } } }
Единственным отличием этого метода от метода, описанного в подразделе 3.2, является элемент jvmArgs . Его значение -XX:-StackTraceInThrowable является опцией JVM, не позволяющей добавить трассировку стека в исключение.
Давайте снова запустим тесты:
Benchmark Mode Cnt Score Error Units ExceptionBenchmark.createExceptionWithoutThrowingIt avgt 10 17.874 ± 3.199 ms/op ExceptionBenchmark.doNotThrowException avgt 10 0.046 ± 0.003 ms/op ExceptionBenchmark.throwAndCatchException avgt 10 16.268 ± 0.239 ms/op ExceptionBenchmark.throwExceptionWithoutAddingStackTrace avgt 10 1.174 ± 0.014 ms/op
Не заполняя исключение трассировкой стека, мы сократили продолжительность выполнения более чем в 100 раз. По-видимому, прохождение по стеку и добавление его кадров к исключению приводит к вялости, которую мы видели.
3.5. Выбрасывание исключения и Разматывание его трассировки стека
Наконец, давайте посмотрим, что произойдет, если мы создадим исключение и размотаем трассировку стека при его обнаружении:
@Benchmark public void throwExceptionAndUnwindStackTrace(Blackhole blackhole) { for (int i = 0; i < LIMIT; i++) { try { throw new Exception(); } catch (Exception e) { blackhole.consume(e.getStackTrace()); } } }
Вот результат:
Benchmark Mode Cnt Score Error Units ExceptionBenchmark.createExceptionWithoutThrowingIt avgt 10 16.605 ± 0.988 ms/op ExceptionBenchmark.doNotThrowException avgt 10 0.047 ± 0.006 ms/op ExceptionBenchmark.throwAndCatchException avgt 10 16.449 ± 0.304 ms/op ExceptionBenchmark.throwExceptionAndUnwindStackTrace avgt 10 326.560 ± 4.991 ms/op ExceptionBenchmark.throwExceptionWithoutAddingStackTrace avgt 10 1.185 ± 0.015 ms/op
Просто развернув трассировку стека, мы видим колоссальное увеличение продолжительности выполнения примерно в 20 раз. Другими словами, производительность намного хуже, если мы извлекаем трассировку стека из исключения в дополнение к его выбрасыванию.
4. Заключение
В этом уроке мы проанализировали влияние исключений на производительность. В частности, выяснилось, что затраты на производительность в основном связаны с добавлением трассировки стека к исключению. Если впоследствии эта трассировка стека будет размотана, накладные расходы станут намного больше.
Поскольку выбрасывание и обработка исключений обходятся дорого, мы не должны использовать его для обычных потоков программ. Вместо этого, как следует из его названия, исключения следует использовать только в исключительных случаях.
Полный исходный код можно найти на GitHub .