Как правильно написать микро-бенчмарк на Java? Ловушки JIT и оптимизаций
Почему ваш бенчмарк не работает
Ваш код страдает от классических проблем наивных микро-бенчмарков:
- JIT-компиляция не прогрета — JVM сначала интерпретирует код, потом компилирует. Замеры включают время компиляции.
- Dead Code Elimination (DCE) — переменная
sumнигде не используется, JIT может выкинуть весь цикл. - Сборка мусора (GC) — создание 100 000 списков и массивов вызывает GC, что искажает время.
- Начальная инициализация — Class loading, allocation overhead.
Правильный подход: JMH (Java Microbenchmark Harness)
Это стандартный инструмент от OpenJDK. Он автоматически решает все проблемы. Вот как исправить ваш бенчмарк:
import org.openjdk.jmh.annotations.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Thread)
public class MyCorrectBenchmark {
@Benchmark
public int arrayListSum() {
List<Integer> list = new ArrayList<>();
for (int j = 0; j < 1000; j++) {
list.add(j);
}
int sum = 0;
for (int val : list) {
sum += val;
}
return sum; // Возвращаем результат, чтобы избежать DCE
}
@Benchmark
public int arraySum() {
int[] arr = new int[1000];
for (int j = 0; j < 1000; j++) {
arr[j] = j;
}
int sum = 0;
for (int val : arr) {
sum += val;
}
return sum;
}
}
Как запустить JMH
Добавьте зависимость в Maven/Gradle:
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.36</version>
</dependency>
Запустите через main:
public class Main {
public static void main(String[] args) throws Exception {
org.openjdk.jmh.Main.main(args);
}
}
Что делает JMH под капотом
- Прогрев (Warmup) — выполняет код несколько итераций без замера, чтобы JIT скомпилировал и оптимизировал.
- Fork — запускает тесты в отдельных JVM, чтобы избежать влияния предыдущих тестов.
- Blackhole — потребляет результат, чтобы JIT не выкинул код.
- Учёт GC — можно настроить паузы для сборки мусора.
Ручные правила (если JMH недоступен)
- Прогрев: выполните тест 10-20 раз до замера.
- Используйте результат: сохраняйте в поле или печатайте.
- System.gc() перед замером — не гарантирует, но помогает.
- Много итераций: не менее 10-20 замеров, усредняйте.
Итог
Ваш исходный код ненадёжен. Используйте JMH — это сэкономит часы отладки. Помните: микро-бенчмаркинг — это искусство, и даже JMH не даёт абсолютной истины, но он максимально приближает к реальности.
<<Альтернативный подход: тестирование на макроуровне
Если вам не нужна микроскопическая точность, а важно сравнить два подхода в реальном приложении, лучше написать интеграционный тест или нагрузочный тест с реальными данными. Микро-бенчмарки часто вводят в заблуждение, потому что изолируют код от контекста (кэши, GC, конкуренция).
Вот простой способ без JMH, который хотя бы учитывает прогрев:
public class SimpleBenchmark {
public static void main(String[] args) {
// Прогрев
for (int i = 0; i < 10000; i++) {
testArrayList();
testArray();
}
// Замер
long total = 0;
for (int i = 0; i < 100; i++) {
total += testArrayList();
}
System.out.println("Avg ArrayList: " + (total / 100));
// Аналогично для массива
}
static long testArrayList() {
long start = System.nanoTime();
List<Integer> list = new ArrayList<>();
for (int j = 0; j < 1000; j++) list.add(j);
int sum = 0;
for (int v : list) sum += v;
return System.nanoTime() - start;
}
}
Этот подход не идеален, но даёт более стабильные результаты, чем ваш первоначальный код. Однако для серьёзных замеров всё равно рекомендую JMH.
Ответов пока нет.
Войдите чтобы оставить ответ