OutOfMemory с JMH и режимом.AverageTime
Я пишу микро-бенчмарк для сравнения конкатенации строк с помощью + оператор vs StringBuilder . С этой целью я создал эталонный класс JMH на основе примера OpenJDK, который использует параметр batchSize :
@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@Measurement(batchSize = 10000, iterations = 10)
@Warmup(batchSize = 10000, iterations = 10)
@Fork(1)
public class StringConcatenationBenchmark {
private String string;
private StringBuilder stringBuilder;
@Setup(Level.Iteration)
public void setup() {
string = "";
stringBuilder = new StringBuilder();
}
@Benchmark
public void stringConcatenation() {
string += "some more data";
}
@Benchmark
public void stringBuilderConcatenation() {
stringBuilder.append("some more data");
}
}
При запуске бенчмарка я получаю следующую ошибку для метода stringBuilderConcatenation
:
java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3332)
at java.lang.AbstractStringBuilder.expandCapacity(AbstractStringBuilder.java:137)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:121)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:421)
at java.lang.StringBuilder.append(StringBuilder.java:136)
at link.pellegrino.string_concatenation.StringConcatenationBenchmark.stringBuilderConcatenation(StringConcatenationBenchmark.java:29)
at link.pellegrino.string_concatenation.generated.StringConcatenationBenchmark_stringBuilderConcatenation.stringBuilderConcatenation_avgt_jmhStub(StringConcatenationBenchmark_stringBuilderConcatenation.java:165)
at link.pellegrino.string_concatenation.generated.StringConcatenationBenchmark_stringBuilderConcatenation.stringBuilderConcatenation_AverageTime(StringConcatenationBenchmark_stringBuilderConcatenation.java:130)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at org.openjdk.jmh.runner.BenchmarkHandler$BenchmarkTask.call(BenchmarkHandler.java:430)
at org.openjdk.jmh.runner.BenchmarkHandler$BenchmarkTask.call(BenchmarkHandler.java:412)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
Я думал, что размер кучи JVM по умолчанию должен быть увеличен, поэтому я попытался разрешить до 10 ГБ, используя значение -Xmx10G
с -jvmArgs
вариант, предоставленный JMH. К сожалению, я все еще получаю ошибку.
Следовательно, я попытался уменьшить значение параметра batchSize
до 1 но я все равно получаюOutOfMemoryError .
Единственное решение, которое я нашел, - это установить режим бенчмарка в Mode.SingleShotTime
. Поскольку этот режим, по-видимому, рассматривает пакет как один выстрел (даже если s/op отображается в столбце единицы измерения), мне кажется, что я получаю метрику, которую я хочу: среднее время выполнения набора пакетов оперативный. Однако я до сих пор не понимаю, почему он не работает с Mode.AverageTime
.
Пожалуйста, Также обратите внимание, что бенчмарки для метода stringConcatenation
работают, как и ожидалось, независимо от используемого режима бенчмарка. Проблема возникает только с stringBuilderConcatenation
методом, использующим StringBuilder.
Любая помощь, чтобы понять, почему предыдущий пример не работает с эталонным режимом, установленным в Mode.AverageTime
, приветствуется.
Версия JMH, которую я использовал, является 1.10.4.
1 ответ:
Вы правы, что
Mode.SingleShotTime
- это то, что вам нужно: он измеряет время для одной партии. При использованииMode.AverageTime
Ваша итерация будет работать до тех пор, пока не закончится время итерации (которое по умолчанию составляет 1 секунду). Он измеряет время на выполнение одной партии (учитываются только партии, которые были полностью завершены во время выполнения), поэтому конечные результаты отличаются, но время выполнения одно и то же.Другая проблема заключается в том, что
@Setup(Level.Iteration)
заставляет установку выполняться перед каждой итерацией, но не перед каждой партией. Таким образом, ваши строки фактически не ограничены размером пакета. Строковая версия не вызываетOutOfMemoryError
только потому, что она намного медленнее, чемStringBuilder
, поэтому в течение 1 секунды она способна построить гораздо более короткую строку.Не очень красивый способ исправить ваш бенчмарк (все еще используя режим среднего времени и параметр batchSize) - это сбросить строку / stringBuilder вручную:
@State(Scope.Thread) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.MICROSECONDS) @Measurement(batchSize = 10000, iterations = 10) @Warmup(batchSize = 10000, iterations = 10) @Fork(1) public class StringConcatenationBenchmark { private static final String S = "some more data"; private static final int maxLen = S.length()*10000; private String string; private StringBuilder stringBuilder; @Setup(Level.Iteration) public void setup() { string = ""; stringBuilder = new StringBuilder(); } @Benchmark public void stringConcatenation() { if(string.length() >= maxLen) string = ""; string += S; } @Benchmark public void stringBuilderConcatenation() { if(stringBuilder.length() >= maxLen) stringBuilder = new StringBuilder(); stringBuilder.append(S); } }
Вот результаты на моем ящике (i5 3340, 4GB RAM, 64bit Win7, JDK 1.8.0_45):
Benchmark Mode Cnt Score Error Units stringBuilderConcatenation avgt 10 145.997 ± 2.301 us/op stringConcatenation avgt 10 324878.341 ± 39824.738 us/op
Таким образом, вы можете видеть, что только около 3 партий подходят для второго
stringConcatenation
(1e6/324878
) в то время как дляstringBuilderConcatenation
могут быть выполнены тысячи пакетов, что приводит к огромной строке, ведущей кOutOfMemoryError
.Я не знаю, почему добавление дополнительной памяти не работает для вас, для меня
-Xmx4G
достаточно запустить тест stringBuilder вашего исходного бенчмарка. Вероятно, ваш ящик быстрее, поэтому результирующая строка еще длиннее. Обратите внимание, что для очень большой строки вы можете попасть в ограничение размера массива (2 миллиарды элементов), даже если у вас достаточно памяти. Проверьте исключение stacktrace после добавления памяти: это то же самое? Если вы нажмете ограничение на размер массива, он все равно будетOutOfMemoryError
, но stacktrace будет немного отличаться. В любом случае, даже при достаточном объеме памяти результаты для вашего бенчмарка будут неверными (как дляString
, так и дляStringBuilder
).