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

Другие вопросы по тегам