Оригинал: Miss Sister Taste (идентификатор публичной учетной записи WeChat: xjjdog), добро пожаловать, пожалуйста, сохраните источник для перепечатки.
Недавно я пишу генератор идентификаторов и мне нужно сравнитьUUID
и тем популярнееNanoID
Разница в скорости между ними, конечно, тоже нужна для проверки генератора ID, созданного по правилам.
Такой код относится к самому базовому API, и даже если скорость уменьшить на несколько наносекунд, накопление очень впечатляет. Дело в том, как я могу оценить скорость генерации ID?
1. Как измерить производительность?
Обычный подход заключается в написании некоторого статистического кода. Этот код, перемежающийся с нашей логикой, выполняет несколько простых операций синхронизации. Например, следующие строки:
long start = System.currentTimeMillis();
//logic
long cost = System.currentTimeMillis() - start;
System.out.println("Logic cost : " + cost);
Этот вид статистического метода не обязательно является проблемой при использовании в бизнес-коде, даже в APM.
К сожалению, статистические результаты этого кода не всегда точны. Например, когда JVM выполняется, она будет выполнять JIT-компиляцию и встроенную оптимизацию некоторых блоков кода или какой-либо часто выполняемой логики.上万次
, согревать. Разница в производительности до и после прогрева очень велика.
Кроме того, существует множество метрик для оценки производительности. Если эти индикаторные данные приходится каждый раз рассчитывать вручную, это должно быть утомительно и неэффективно.
JMH (Java Microbenchmark Harness) — такой инструмент для бенчмаркинга. Если вы обнаружили горячий код с помощью нашей серии инструментов и хотите проверить данные о его производительности и оценить улучшение, вы можете передать его в JMH. Точность его измерений очень высока, вплоть до наносекундного уровня.
JMH был включен в JDK 12. Другие версии должны быть введены в maven самостоятельно.Координаты следующие.
<dependencies>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.23</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.23</version>
<scope>provided</scope>
</dependency>
</dependencies>
Ниже мы расскажем об использовании этого инструмента.
2. Ключевые примечания
JMH
это пакет jar, он и среда модульного тестированияJUnit
Очень похоже, что некоторую базовую настройку можно выполнить с помощью аннотаций. В этой части есть много конфигураций, доступ к которым можно получить через основной методOptionsBuilder
настраивать.
На картинке выше показано, что выполняет типичная программа JMH. Открыв несколько процессов и несколько потоков, сначала выполните прогрев, затем выполните итерацию и, наконец, обобщите все тестовые данные для анализа. До и после выполнения некоторые предварительные и последующие операции также могут быть обработаны в соответствии с степенью детализации.
Простой код выглядит следующим образом:
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Thread)
@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(1)
@Threads(2)
public class BenchmarkTest {
@Benchmark
public long shift() {
long t = 455565655225562L;
long a = 0;
for (int i = 0; i < 1000; i++) {
a = t >> 30;
}
return a;
}
@Benchmark
public long div() {
long t = 455565655225562L;
long a = 0;
for (int i = 0; i < 1000; i++) {
a = t / 1024 / 1024 / 1024;
}
return a;
}
public static void main(String[] args) throws Exception {
Options opts = new OptionsBuilder()
.include(BenchmarkTest.class.getSimpleName())
.resultFormat(ResultFormatType.JSON)
.build();
new Runner(opts).run();
}
}
Ниже мы последовательно вводим ключевые аннотации и параметры.
@Warmup
Образец.
@Warmup(
iterations = 5,
time = 1,
timeUnit = TimeUnit.SECONDS)
Мы упоминали предварительный нагрев не раз,warmup
Эту аннотацию можно использовать в классах или методах для предварительной настройки. Как видите, у него есть несколько параметров конфигурации.
-
timeUnit
: Единица времени, единица измерения по умолчанию — секунды. -
iterations
: количество итераций на этапе прогрева. -
time
: время каждой разминки. -
batchSize
: размер пакета, указывающий, сколько раз метод вызывается за операцию.
Приведенная выше аннотация означает, что код прогревается в общей сложности 5 секунд (5 итераций по одной секунде каждая). Данные испытаний процесса предварительного нагрева не записываются.
Мы можем взглянуть на эффект от его выполнения:
# Warmup: 3 iterations, 1 s each
# Warmup Iteration 1: 0.281 ops/ns
# Warmup Iteration 2: 0.376 ops/ns
# Warmup Iteration 3: 0.483 ops/ns
Как правило, эталонные тесты нацелены на относительно небольшие фрагменты кода, которые выполняются относительно быстро. Этот код имеет высокую вероятность компилирования, встраивания и сохранения компактности метода при кодировании, что также хорошо для JIT.
Говоря о прогреве, мы должны упомянуть прогрев сервисов в распределенной среде. При публикации сервисных узлов обычно происходит процесс прогрева и постепенное увеличение объема на соответствующие сервисные узлы, пока сервис не достигнет оптимального состояния. Как показано на рисунке ниже, за этот процесс увеличения объема отвечает балансировка нагрузки, и он обычно основан на процентах.
@Measurement
Пример следующий.
@Measurement(
iterations = 5,
time = 1,
timeUnit = TimeUnit.SECONDS)
Measurement
иWarmup
Параметры те же. В отличие от прогрева, это относится к фактическому количеству итераций.
Мы можем видеть этот процесс выполнения из журнала:
# Measurement: 5 iterations, 1 s each
Iteration 1: 1646.000 ns/op
Iteration 2: 1243.000 ns/op
Iteration 3: 1273.000 ns/op
Iteration 4: 1395.000 ns/op
Iteration 5: 1423.000 ns/op
Хотя код может показать свое оптимальное состояние после прогрева, все же есть некоторые расхождения между общими и практическими сценариями применения. Если производительность вашей тестовой машины очень высока или использование ресурсов вашей тестовой машины достигло своего предела, это повлияет на значение результата теста. В обычных условиях я предоставлю машине достаточно ресурсов для поддержания стабильной среды при тестировании. При анализе результатов мы также уделяем больше внимания различным методам реализации.разница в производительности, а не сами тестовые данные.
@BenchmarkMode
Эта аннотация используется для указания типа теста, соответствующего параметру Mode, который можно использовать для украшения классов и методов. Значение здесь представляет собой массив, который может настраивать несколько статистических измерений. Например:
@BenchmarkMode({Throughput,Mode.AverageTime})
. Статистика — пропускная способность и среднее время выполнения.
Так называемый режим в JMH можно разделить на следующие:
- Пропускная способность:Общая пропускная способность, такая как число запросов в секунду, объем вызовов в единицу времени и т. д.
- Среднее время:Среднее время относится к среднему времени каждого выполнения. Если это значение слишком мало для распознавания, вы можете настроить единичное время статистики на меньшее значение.
-
Время выборки:случайный
取样
. - Время одиночного снимка:Если вы хотите проверить производительность только один раз, например, сколько времени требуется для инициализации в первый раз, вы можете использовать этот параметр, который ничем не отличается от традиционного основного метода.
- Все:Все индикаторы рассчитываются один раз, вы можете установить этот параметр, чтобы увидеть эффект.
Возьмем среднее время и посмотрим на общий результат выполнения:
Result "com.github.xjjdog.tuning.BenchmarkTest.shift":
2.068 ±(99.9%) 0.038 ns/op [Average]
(min, avg, max) = (2.059, 2.068, 2.083), stdev = 0.010
CI (99.9%): [2.030, 2.106] (assumes normal distribution)
Поскольку единицей времени, которую мы объявляем, являются наносекунды, среднее время отклика этого метода сдвига составляет 2,068 наносекунды.
Мы также можем посмотреть на окончательное прошедшее время.
Benchmark Mode Cnt Score Error Units
BenchmarkTest.div avgt 5 2.072 ± 0.053 ns/op
BenchmarkTest.shift avgt 5 2.068 ± 0.038 ns/op
Поскольку это среднее значение, значение ошибки здесь является значением ошибки (или флуктуации).
Видно, что при измерении этих показателей присутствует измерение времени, которое настраивается через аннотацию **@OutputTimeUnit**.
Это относительно просто, оно указывает тип времени результата теста. Может использоваться в классах или методах. Обычно выбирают секунды, миллисекунды, микросекунды, наносекунды — очень быстрые методы.
Например,@BenchmarkMode(Mode.Throughput)
и@OutputTimeUnit(TimeUnit.MILLISECONDS)
В совокупности он представляет собой пропускную способность в миллисекунду.
Приведенные ниже результаты пропускной способности измеряются в миллисекундах.
Benchmark Mode Cnt Score Error Units
BenchmarkTest.div thrpt 5 482999.685 ± 6415.832 ops/ms
BenchmarkTest.shift thrpt 5 480599.263 ± 20752.609 ops/ms
OutputTimeUnit
Аннотации также могут изменять классы или методы, и, изменяя уровень времени, можно получить более читаемые результаты.
@Fork
Значение fork обычно устанавливается равным 1, что означает, что для тестирования используется только один процесс; если это число больше 1, это означает, что для тестирования будет включен новый процесс; но если оно установлено в 0, программа по-прежнему будет работать, но это в процессе JVM пользователя.Вы можете увидеть следующие советы, но делать это не рекомендуется.
# Fork: N/A, test runs in the host VM
# *** WARNING: Non-forked runs may silently omit JVM options, mess up profilers, disable compiler hints, etc. ***
# *** WARNING: Use non-forked runs only for debugging purposes, not for actual performance runs. ***
Так работает ли форк в среде процессов или потоков? Давайте проследим исходный код JMH и обнаружим, что каждый процесс ветвления выполняется отдельно вProccess
В процессе, чтобы можно было сделать полную изоляцию среды, чтобы избежать перекрестного влияния. Его входные и выходные потоки через режим подключения Socket отправляются на наш исполнительный терминал.
Поделитесь небольшой хитростью здесь. На самом деле аннотация форка имеет параметр с именемjvmArgsAppend
, через который мы можем передать некоторые аргументы JVM.
@Fork(value = 3, jvmArgsAppend = {"-Xmx2048m", "-server", "-XX:+AggressiveOpts"})
В обычных тестах количество разветвлений также может быть соответствующим образом увеличено, чтобы уменьшить погрешность теста.
@Threads
fork
ориентирована на процесс, в то время какThreads
ориентирован на потоки. При указании этой аннотации будет включено параллельное тестирование.
Если настроен Threads.MAX, используйте такое же количество потоков, как и ядер процессора.
@Group
Аннотацию @Group можно добавлять только к методам для категоризации тестовых методов. Вы можете использовать эту аннотацию, если у вас есть много методов в одном тестовом файле или если вам нужно их классифицировать.
связанные с ним@GroupThreads
Аннотация, на основании этой классификации, сделает некоторые настройки потока.
@State
@State указывает область действия переменной внутри класса. Он имеет три значения.
@State используется для объявления того, что класс является «состоянием», а параметр Scope может использоваться для представления общей области действия состояния. Эту аннотацию необходимо добавить в класс, иначе подсказка работать не будет.
Scope имеет следующие три значения:
- Benchmark: указывает, что областью действия переменной является эталонный класс.
- Thread: копия каждого потока, если настроена аннотация Threads, у каждого Thread есть переменная, и они не влияют друг на друга.
- Group: Обратитесь к аннотации @Group выше, в той же группе будет использоваться один и тот же экземпляр переменной.
существуетJMHSample04DefaultState
В тестовом файле демонстрируется переменнаяx
Областью действия по умолчанию является Thread, а код ключа выглядит следующим образом:
@State(Scope.Thread)
public class JMHSample_04_DefaultState {
double x = Math.PI;
@Benchmark
public void measure() {
x++;
}
}
@Setup и @TearDown
Подобно среде модульного тестирования JUnit, он используется для действий по инициализации перед бенчмаркингом, а @TearDown используется для действий после бенчмаркинга для выполнения некоторой глобальной конфигурации.
Эти две аннотации также имеют значение уровня, которое указывает, когда запускается метод, и имеет три значения.
- Trial: уровень по умолчанию. То есть уровень Benchmark.
- Iteration: будет запускаться каждую итерацию.
- Invocation: будет запускаться каждый раз при вызове метода, это наиболее детализированный.
@Param
Аннотация @Param может изменять поля только для проверки влияния различных параметров на производительность программы. С помощью аннотации @State область выполнения этих параметров может быть указана одновременно.
Пример кода выглядит следующим образом:
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(1)
@State(Scope.Benchmark)
public class JMHSample_27_Params {
@Param({"1", "31", "65", "101", "103"})
public int arg;
@Param({"0", "1", "2", "4", "8", "16", "32"})
public int certainty;
@Benchmark
public boolean bench() {
return BigInteger.valueOf(arg).isProbablePrime(certainty);
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JMHSample_27_Params.class.getSimpleName())
// .param("arg", "41", "42") // Use this to selectively constrain/override parameters
.build();
new Runner(opt).run();
}
}
Стоит отметить, что если вы установите очень большое количество параметров, эти параметры будут выполняться несколько раз, как правило, в течение длительного времени. Например, если имеется 1 M параметров и 2 N параметров, то в общей сложности необходимо выполнить M*N раз.
Ниже скриншот результата выполнения.
@CompilerControl
Можно сказать, что это очень полезная функция.
Накладные расходы на вызовы методов в Java относительно велики, особенно когда количество вызовов очень велико. Возьмем простые методы получения/установки, которых в Java-коде предостаточно. При доступе нам нужно создать соответствующий кадр стека, а после доступа к необходимым полям открыть кадр стека и возобновить выполнение исходной программы.
Если доступ и работа с этими объектами могут быть перенесены в область вызова целевого метода, будет на один вызов метода меньше, а скорость улучшится.Это концепция встраивания методов. Как показано на рисунке, после JIT-компиляции кода эффективность значительно повысится.
Эту аннотацию можно использовать в классах или методах для управления поведением компиляции методов.Существует три широко используемых режима.
Принудительно использовать встраивание (INLINE), запретить использование встраивания (DONT_INLINE) и даже запретить компиляцию метода (EXCLUDE) и т. д.
2. График результатов
Используя результаты теста JMH, его можно повторно обработать и отобразить графически. В сочетании с данными диаграммы это более интуитивно понятно. Указав файл выходного формата во время выполнения, можно получить результаты теста производительности в соответствующем формате.
Например, следующая строка кода задает выходные данные в формате JSON.
Options opt = new OptionsBuilder()
.resultFormat(ResultFormatType.JSON)
.build();
JMH поддерживает результаты в следующих 5 форматах:
- TEXTЭкспорт текстового файла.
- CSVЭкспорт в файл формата csv.
- SCSVЭкспорт файлов в такие форматы, как scsv.
- JSONЭкспорт в json-файл.
- LATEXЭкспорт в латекс, систему набора текста, основанную на TEX.
Вообще говоря, мы экспортируем в файл CSV, работаем непосредственно в Excel и генерируем соответствующую графику.
Кроме того, есть несколько инструментов, которые можно использовать для построения графиков:
JMH VisualizerВот проект с открытым исходным кодом (встреча.больше, чем.IO/), экспортировав файл json, после загрузки вы можете получить простые статистические результаты. Лично я не думаю, что это показано очень хорошо.
jmh-visual-chart
Для сравнения, следующий инструмент (глубокий ОО VE.com/meet-visual-…, что относительно просто.
meta-chart
Универсальный онлайн-генератор диаграмм. (www.meta-chart.com/), после экспорта файла CSV…
Некоторые инструменты непрерывной интеграции, такие как Jenkins, также предоставляют соответствующие подключаемые модули для непосредственного отображения результатов этих тестов.
END
Этот инструмент очень полезен, он использует точные тестовые данные для поддержки результатов нашего анализа. В общем, если вы обнаружите горячий код, вам нужно использовать инструменты бенчмаркинга для специальной оптимизации, пока производительность не будет значительно улучшена.
В нашем сценарии мы обнаружили, что использование NanoID действительно намного быстрее, чем UUID.
Об авторе: Miss Sister Taste (xjjdog), общедоступный аккаунт, который не позволяет программистам идти в обход. Сосредоточьтесь на инфраструктуре и Linux. Десять лет архитектуры, десятки миллиардов ежедневного трафика, обсуждение с вами мира высокой параллелизма, дающие вам другой вкус. Мой личный WeChat xjjdog0, добро пожаловать в друзья для дальнейшего общения.