OutOfMemory с JMH и Mode.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.
Любая помощь, чтобы понять, почему предыдущий пример не работает с режимом Benchmark, установленным в 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, 4 Гб оперативной памяти, 64-битная 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 миллиарда элементов), даже если у вас достаточно памяти. Проверьте трассировку стека исключений после добавления памяти: это то же самое? Если вы достигнете предела размера массива, он все равно будет OutOfMemoryError
, но stacktrace будет немного отличаться. В любом случае, даже при достаточном объеме памяти результаты вашего теста будут неверными (как для String
а также StringBuilder
).