Как мне написать правильный микро-тест в Java?

870

Как вы пишете (и запускаете) правильный микро-тест в Java?

Я ищу некоторые примеры кода и комментарии, иллюстрирующие различные вещи, чтобы думать.

Пример: должен ли эталон измерять время / итерацию или итерации / время и почему?

Связанный: приемлем ли сравнительный анализ секундомера?

Джон Нильссон
источник
Смотрите [этот вопрос] [1] несколько минут назад для получения дополнительной информации. редактировать: извините, это не должно быть ответом. Я должен был оставить комментарий. [1]: stackoverflow.com/questions/503877/…
Tiago
После того, как я планировал отнести автора этого вопроса к такому вопросу, я заметил, что этого вопроса не существует. Так что, надеюсь, со временем он соберет несколько полезных советов.
Джон Нильссон
5
Java 9 может предоставлять некоторые функции для микробенчмаркинга
Raedwald
1
@Raedwald Я думаю, что этот JEP направлен на то, чтобы добавить некоторый микро-тест в код JDK, но я не думаю, что JMH будет включен в JDK ...
assylias
1
@Raedwald Привет из будущего. Это не сделало сокращение .
Майкл

Ответы:

787

Советы по написанию микро-тестов от создателей Java HotSpot :

Правило 0: Прочтите авторитетную статью о JVM и микробенчмаркинге. Хорошим является Брайан Гетц, 2005 . Не ожидайте слишком многого от микро-тестов; они измеряют только ограниченный диапазон рабочих характеристик JVM.

Правило 1: всегда включайте фазу разминки, которая запускает ваше тестовое ядро ​​на всем протяжении, достаточное для запуска всех инициализаций и компиляций до фазы (фаз) синхронизации. (Меньше итераций в порядке на этапе разогрева. Основное правило - несколько десятков тысяч итераций внутреннего цикла.)

Правило 2: всегда выполняйте с -XX:+PrintCompilation, -verbose:gcи т. Д., Чтобы вы могли убедиться, что компилятор и другие части JVM не выполняют неожиданную работу во время фазы синхронизации.

Правило 2.1. Печатайте сообщения в начале и в конце фаз синхронизации и прогрева, чтобы можно было убедиться, что в фазе синхронизации нет выходных данных из правила 2.

Правило 3: помните о разнице между -clientи -server, и OSR, и регулярными компиляциями. -XX:+PrintCompilationФлаг сообщает ЛРН компиляций с при-знаком для обозначения без начальной точки входа, например: Trouble$1::run @ 2 (41 bytes). Предпочитайте сервер клиенту, а обычное - OSR, если вы стремитесь к лучшей производительности.

Правило 4: знать об эффектах инициализации. Не печатайте в первый раз во время фазы синхронизации, так как печать загружает и инициализирует классы. Не загружайте новые классы за пределами фазы прогрева (или финальной фазы отчетности), если только вы не тестируете загрузку классов специально (а в этом случае загружаете только тестовые классы). Правило 2 - ваша первая линия защиты от таких эффектов.

Правило 5: знать о последствиях деоптимизации и перекомпиляции. Не используйте какой-либо путь к коду в первый раз на этапе синхронизации, потому что компилятор может создать нежелательную и перекомпилировать код, основываясь на более раннем оптимистическом предположении, что путь вообще не будет использоваться. Правило 2 - ваша первая линия защиты от таких эффектов.

Правило 6: Используйте соответствующие инструменты, чтобы прочитать мысли компилятора и ожидать, что вы будете удивлены кодом, который он создает. Проверьте код самостоятельно, прежде чем создавать теории о том, что делает что-то быстрее или медленнее.

Правило 7: уменьшите шум в ваших измерениях. Запустите тест на тихой машине и запустите его несколько раз, отбрасывая выбросы. Используйте -Xbatchдля сериализации компилятора с приложением и рассмотрите возможность настройки, -XX:CICompilerCount=1чтобы компилятор не работал параллельно с самим собой. Старайтесь изо всех сил, чтобы уменьшить накладные расходы GC, установить Xmx(достаточно большой) равных Xmsи использовать, UseEpsilonGCесли он доступен.

Правило 8: используйте библиотеку для своего теста, поскольку она, вероятно, более эффективна и уже отлажена для этой единственной цели. Такие как JMH , Caliper или Билл и превосходные тесты UCSD Пола для Java .

Евгений Кулешов
источник
5
Это была также интересная статья: ibm.com/developerworks/java/library/j-jtp12214
Джон Нильссон
143
Кроме того, никогда не используйте System.currentTimeMillis (), если у вас нет проблем с точностью + или - 15 мс, что типично для большинства комбинаций OS + JVM. Вместо этого используйте System.nanoTime ().
Скотт Кэри
5
Немного бумаги из javaOne: azulsystems.com/events/javaone_2009/session/…
bestsss
94
Следует отметить, что System.nanoTime()не гарантируется, что будет более точным, чем System.currentTimeMillis(). Это гарантированно будет, по крайней мере, так же точно. Однако обычно это значительно точнее.
Гравитация
41
Основная причина, почему нужно использовать System.nanoTime()вместо того System.currentTimeMillis(), чтобы быть, является то, что первое гарантированно будет монотонно увеличиваться. Вычитание значений, возвращаемых двумя currentTimeMillisвызовами, может фактически дать отрицательные результаты, возможно, потому что системное время было настроено некоторым демоном NTP.
Вальдхайнц,
239

Я знаю, что этот вопрос был помечен как ответивший, но я хотел бы упомянуть две библиотеки, которые помогают нам писать микро тесты

Штангенциркуль от Google

Начало обучения

  1. http://codingjunkie.net/micro-benchmarking-with-caliper/
  2. http://vertexlabs.co.uk/blog/caliper

JMH из OpenJDK

Начало обучения

  1. Как избежать ошибок в JVM
  2. http://nitschinger.at/Using-JMH-for-Java-Microbenchmarking
  3. http://java-performance.info/jmh/
Аравинд Яррам
источник
37
+1 это можно было бы добавить как правило 8 принятого ответа: правило 8: поскольку многие вещи могут пойти не так, вам, вероятно, следует использовать существующую библиотеку, а не пытаться делать это самостоятельно!
assylias
8
@Pangea jmh, вероятно, в настоящее время превосходит Caliper, см. Также: groups.google.com/forum/#!msg/mechanical-sympathy/m4opvy4xq3U/…
assylias
87

Важные вещи для тестов Java:

  • Сначала разогрейте JIT, запустив код несколько раз до времени его
  • Убедитесь, что вы используете его достаточно долго, чтобы иметь возможность измерять результаты в секундах или (лучше) десятках секунд
  • Несмотря на то, что вы не можете вызывать System.gc()между итерациями, рекомендуется запускать его между тестами, чтобы каждый тест получал «чистое» пространство памяти для работы. (Да, gc()это скорее подсказка, чем гарантия, но вполне вероятно, что она действительно соберет мусор в моем опыте.)
  • Мне нравится отображать итерации и время, а также счет времени / итерации, который можно масштабировать так, чтобы «лучший» алгоритм получил оценку 1,0, а другие оценивали относительно. Это означает, что вы можете запускать все алгоритмы в течение длительного времени, варьируя как количество итераций, так и время, но при этом получая сопоставимые результаты.

Я только в процессе ведения блога о разработке платформы для тестирования в .NET. У меня есть несколько из предыдущих постов , которые могут быть в состоянии дать вам некоторые идеи - не все будет уместно, конечно, но некоторые из них могут быть.

Джон Скит
источник
3
Незначительная мелочь: IMO «чтобы каждый тест получал» должен быть «так, чтобы каждый тест мог получить», так как первый создает впечатление, что вызов gc всегда освобождает неиспользуемую память.
Санджай Т. Шарма
@ SanjayT.Sharma: Ну, намерение состоит в том, что это действительно так. Хотя это не строго гарантировано, на самом деле это довольно сильный намек. Будет редактировать, чтобы быть понятнее.
Джон Скит
1
Я не согласен с вызовом System.gc (). Это подсказка, вот и все. Даже «мы надеемся что-то сделать». Вы никогда не должны называть это. Это программирование, а не искусство.
gyorgyabraham
13
@gyabraham: Да, это подсказка, но я заметил, что обычно ее принимают. Так что, если вам не нравится использование System.gc(), как вы предлагаете минимизировать сборку мусора в одном тесте из-за объектов, созданных в предыдущих тестах? Я прагматичный, а не догматичный.
Джон Скит
9
@gyabraham: я не знаю, что вы подразумеваете под "большим отступлением". Можете ли вы уточнить, и еще раз - у вас есть предложение, чтобы дать лучшие результаты? Я прямо сказал, что это не гарантия ...
Джон Скит
48

jmh является недавним дополнением к OpenJDK и написано некоторыми инженерами по производительности из Oracle. Конечно, стоит посмотреть.

JMH - это Java-система для построения, запуска и анализа нано / микро / макро тестов, написанных на Java и других языках, предназначенных для JVM.

Очень интересные фрагменты информации скрыты в примерах тестовых комментариев .

Смотрите также:

assylias
источник
1
См. Также этот пост в блоге: psy-lob-saw.blogspot.com/2013/04/… для получения подробной информации о начале работы с JMH.
Ницан Вакарт
К сведению, JEP 230: Microbenchmark Suite - это предложение OpenJDK, основанное на этом проекте Java Microbenchmark Harness (JMH) . Не сделал сокращение для Java 9, но может быть добавлено позже.
Василий Бурк
23

Должен ли эталон измерять время / итерацию или итерации / время и почему?

Это зависит от того, что вы пытаетесь проверить.

Если вас интересует задержка , используйте время / итерацию, а если вас интересует пропускная способность , используйте итерации / время.

Питер Лори
источник
16

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

for(i=1..n)
  alg1();
for(i=1..n)
  alg2();
for(i=1..n)
  alg2();
for(i=1..n)
  alg1();

Я обнаружил некоторые заметные различия (иногда 5-10%) во время выполнения одного и того же алгоритма на разных проходах.

Кроме того, убедитесь, что n очень велико, чтобы время выполнения каждого цикла составляло как минимум 10 секунд или около того. Чем больше итераций, тем значительнее показатели времени тестирования и тем надежнее данные.

койка
источник
5
Естественно, изменение порядка влияет на время выполнения. JVM-оптимизации и кеширование будут работать здесь. Лучше «прогреть» JVM-оптимизацию, выполнить несколько прогонов и сравнить каждый тест в другой JVM.
Mnementh
15

Убедитесь, что вы каким-то образом используете результаты, которые вычисляются в тестируемом коде. В противном случае ваш код может быть оптимизирован.

Петр Штибраны
источник
13

Существует множество возможных подводных камней для написания микро-тестов в Java.

Во-первых: Вы должны рассчитывать со всеми видами событий, которые занимают более или менее случайное время: сборка мусора, эффекты кэширования (ОС для файлов и ЦП для памяти), IO и т. Д.

Второе: вы не можете доверять точности измеренного времени для очень коротких интервалов.

Третье: JVM оптимизирует ваш код во время выполнения. Так что разные прогоны в одном и том же JVM-экземпляре будут становиться все быстрее и быстрее.

Мои рекомендации: сделайте тест производительности за несколько секунд, это более надежно, чем время выполнения за миллисекунды. Прогрейте JVM (это означает, что хотя бы один раз тест будет выполнен без измерения, чтобы JVM могла выполнять оптимизацию). И проведите свой тест несколько раз (возможно, 5 раз) и возьмите среднее значение. Запустите каждый микропроцессор в новом экземпляре JVM (вызовите каждый тест нового Java), иначе эффекты оптимизации JVM могут повлиять на последующие выполняемые тесты. Не выполняйте вещи, которые не выполняются в фазе разогрева (так как это может вызвать загрузку классов и перекомпиляцию).

Mnementh
источник
8

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

Это связано с тем, что реализация Aможет быть быстрее во время большинства прогонов теста, чем реализация B. Но Aможет также иметь более высокий разброс, поэтому измеренный выигрыш в производительности Aне будет иметь никакого значения по сравнению с B.

Поэтому важно также правильно написать и запустить микро-тест, а также правильно его проанализировать.

SpaceTrucker
источник
8

В дополнение к другим отличным советам, я бы также помнил следующее:

Для некоторых процессоров (например, диапазон Intel Core i5 с TurboBoost) температура (и количество используемых в настоящее время ядер, а также процент их использования) влияет на тактовую частоту. Поскольку процессоры синхронизируются динамически, это может повлиять на ваши результаты. Например, если у вас однопоточное приложение, максимальная тактовая частота (с TurboBoost) выше, чем для приложения, использующего все ядра. Поэтому это может помешать сравнениям однопоточной и многопоточной производительности в некоторых системах. Имейте в виду, что температура и напряжение также влияют на то, как долго поддерживается турбо частота.

Возможно, более принципиально важный аспект, над которым у вас есть прямой контроль: убедитесь, что вы измеряете правильную вещь! Например, если вы используете System.nanoTime()для сравнения определенного фрагмента кода, размещайте вызовы в назначении в местах, которые имеют смысл, чтобы избежать измерения вещей, которые вас не интересуют. Например, не делайте:

long startTime = System.nanoTime();
//code here...
System.out.println("Code took "+(System.nanoTime()-startTime)+"nano seconds");

Проблема в том, что вы не сразу получаете время окончания, когда код закончен. Вместо этого попробуйте следующее:

final long endTime, startTime = System.nanoTime();
//code here...
endTime = System.nanoTime();
System.out.println("Code took "+(endTime-startTime)+"nano seconds");
Сина Мадани
источник
Да, важно не выполнять несвязанную работу внутри временной области, но ваш первый пример все еще в порядке. Есть только один вызов println, а не отдельная строка заголовка или что-то, и System.nanoTime()должен быть оценен как первый шаг в построении строкового аргумента для этого вызова. С первым компилятор не может ничего поделать со вторым, и никто даже не побуждает их делать дополнительную работу перед записью времени остановки.
Питер Кордес
7

http://opt.sourceforge.net/ Java Micro Benchmark - управляющие задачи, необходимые для определения сравнительных характеристик производительности компьютерной системы на разных платформах. Может использоваться для руководства решениями по оптимизации и для сравнения различных реализаций Java.

Юрий
источник
2
Кажется, просто для сравнения оборудования JVM +, а не произвольный кусок кода Java.
Стефан Л