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 2

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).