Почему StringBuilder#append (int) быстрее в Java 7, чем в Java 8?



во время расследования для спор w. r. t. using "" + n и Integer.toString(int) чтобы преобразовать целочисленный примитив в строку, я написал это JMH microbenchmark:



@Fork(1)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Benchmark)
public class IntStr {
protected int counter;


@GenerateMicroBenchmark
public String integerToString() {
return Integer.toString(this.counter++);
}

@GenerateMicroBenchmark
public String stringBuilder0() {
return new StringBuilder().append(this.counter++).toString();
}

@GenerateMicroBenchmark
public String stringBuilder1() {
return new StringBuilder().append("").append(this.counter++).toString();
}

@GenerateMicroBenchmark
public String stringBuilder2() {
return new StringBuilder().append("").append(Integer.toString(this.counter++)).toString();
}

@GenerateMicroBenchmark
public String stringFormat() {
return String.format("%d", this.counter++);
}

@Setup(Level.Iteration)
public void prepareIteration() {
this.counter = 0;
}
}


я запустил его с параметрами JMH по умолчанию с обоими виртуальными машинами Java, которые существуют на моей машине Linux (современный Mageia 4 64-бит, Процессор Intel i7-3770, 32 ГБ оперативной памяти). Первая Java-машина была одна поставляется с Oracle версии JDK
8u5 64-разрядная:



java version "1.8.0_05"
Java(TM) SE Runtime Environment (build 1.8.0_05-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.5-b02, mixed mode)


С этим JVM я получил в значительной степени то, что я ожидал:



Benchmark                    Mode   Samples         Mean   Mean error    Units
b.IntStr.integerToString thrpt 20 32317.048 698.703 ops/ms
b.IntStr.stringBuilder0 thrpt 20 28129.499 421.520 ops/ms
b.IntStr.stringBuilder1 thrpt 20 28106.692 1117.958 ops/ms
b.IntStr.stringBuilder2 thrpt 20 20066.939 1052.937 ops/ms
b.IntStr.stringFormat thrpt 20 2346.452 37.422 ops/ms


то есть с помощью StringBuilder класс медленнее из-за дополнительных накладных расходов на создание StringBuilder объект и добавление пустой строки. Используя String.format(String, ...) еще медленнее, на порядок или около того.



компилятор, предоставляемый дистрибутивом, с другой стороны, основан на OpenJDK 1.7:



java version "1.7.0_55"
OpenJDK Runtime Environment (mageia-2.4.7.1.mga4-x86_64 u55-b13)
OpenJDK 64-Bit Server VM (build 24.51-b03, mixed mode)


результаты здесь были интересные:



Benchmark                    Mode   Samples         Mean   Mean error    Units
b.IntStr.integerToString thrpt 20 31249.306 881.125 ops/ms
b.IntStr.stringBuilder0 thrpt 20 39486.857 663.766 ops/ms
b.IntStr.stringBuilder1 thrpt 20 41072.058 484.353 ops/ms
b.IntStr.stringBuilder2 thrpt 20 20513.913 466.130 ops/ms
b.IntStr.stringFormat thrpt 20 2068.471 44.964 ops/ms


почему StringBuilder.append(int) появляются намного быстрее с этим JVM? Глядя на StringBuilder исходный код класса не выявил ничего особенно интересного-метод, о котором идет речь, почти идентичен Integer#toString(int). Что интересно, добавляя результат Integer.toString(int) (the stringBuilder2 microbenchmark) не кажется, чтобы быть быстрее.



является ли это несоответствие производительности проблемой с тестовым жгутом? Или мой OpenJDK JVM содержит оптимизации, которые повлияют на этот конкретный код (анти)-шаблон?



EDIT:



для более прямого сравнения я установил Oracle JDK 1. 7u55:



java version "1.7.0_55"
Java(TM) SE Runtime Environment (build 1.7.0_55-b13)
Java HotSpot(TM) 64-Bit Server VM (build 24.55-b03, mixed mode)


результаты аналогичны результатам OpenJDK:



Benchmark                    Mode   Samples         Mean   Mean error    Units
b.IntStr.integerToString thrpt 20 32502.493 501.928 ops/ms
b.IntStr.stringBuilder0 thrpt 20 39592.174 428.967 ops/ms
b.IntStr.stringBuilder1 thrpt 20 40978.633 544.236 ops/ms


кажется, что это более общая проблема Java 7 против Java 8. Возможно, Java 7 имела более агрессивную оптимизацию строк?



EDIT 2:



для полноты описания ниже приведены параметры виртуальной машины, связанные со строками оба эти СПМ:



для Oracle JDK 8u5:



$ /usr/java/default/bin/java -XX:+PrintFlagsFinal 2>/dev/null | grep String
bool OptimizeStringConcat = true {C2 product}
intx PerfMaxStringConstLength = 1024 {product}
bool PrintStringTableStatistics = false {product}
uintx StringTableSize = 60013 {product}


Для OpenJDK 1.7:



$ java -XX:+PrintFlagsFinal 2>/dev/null | grep String
bool OptimizeStringConcat = true {C2 product}
intx PerfMaxStringConstLength = 1024 {product}
bool PrintStringTableStatistics = false {product}
uintx StringTableSize = 60013 {product}
bool UseStringCache = false {product}


The UseStringCache опция была удалена в Java 8 без замены, поэтому я сомневаюсь, что это имеет какое-либо значение. Остальные параметры имеют одинаковые параметры.



EDIT 3:



параллельное сравнение исходного кода AbstractStringBuilder,StringBuilder и Integer классы src.zip файл ничего не показывает примечательно. Помимо целого ряда косметических и документальных изменений,Integer теперь есть некоторая поддержка для целых чисел без знака и StringBuilder был немного переработан, чтобы поделиться больше кода с StringBuffer. Ни одно из этих изменений не влияет на пути кода, используемые StringBuilder#append(int), хотя я что-то пропустил.



сравнение кода сборки, созданного для IntStr#integerToString() и IntStr#stringBuilder0() гораздо интереснее. Основной макет кода, сгенерированного для IntStr#integerToString() был похож на обе виртуальные машины, хотя Oracle версии JDK 8u5, казалось, быть более агрессивным Вт.Р.Т. встраивание некоторые звонки внутри Integer#toString(int) код. Было четкое соответствие с исходным кодом Java, даже для кого-то с минимальным опытом сборки.



код сборки для IntStr#stringBuilder0(), однако, было радикально иначе. Код, сгенерированный Oracle JDK 8u5, снова был напрямую связан с исходным кодом Java - я мог легко распознать тот же макет. Напротив, код, сгенерированный OpenJDK 7 был почти неузнаваем для нетренированного глаза (как и мой). Элемент new StringBuilder() вызов был, казалось бы, удален, как и создание массива в StringBuilder конструктор. Кроме того, плагин дизассемблера не смог предоставить столько ссылок на исходный код, как это было в JDK 8.



я предполагаю, что это либо результат гораздо более агрессивного прохода оптимизации в OpenJDK 7, либо, скорее всего, результат вставки рукописного кода низкого уровня для некоторых StringBuilder оперативный. Я не уверен, почему эта оптимизация не происходит в моей реализации JVM 8 или почему те же оптимизации не были реализованы для Integer#toString(int) в JVM 7. Я думаю, что кто-то, знакомый с соответствующими частями исходного кода JRE, должен был бы ответить на эти вопросы...

705   2  

2 ответов:

TL; DR: побочные эффекты append по-видимому, разбить оптимизацию StringConcat.

очень хороший анализ в исходном вопросе и обновлениях!

для полноты картины ниже приведены несколько шагов:

  • посмотреть через -XX:+PrintInlining как для 7u55, так и для 8u5. В 7u55, вы увидите что-то вроде этого:

     @ 16   org.sample.IntStr::inlineSideEffect (25 bytes)   force inline by CompilerOracle
       @ 4   java.lang.StringBuilder::<init> (7 bytes)   inline (hot)
       @ 18   java.lang.StringBuilder::append (8 bytes)   already compiled into a big method
       @ 21   java.lang.StringBuilder::toString (17 bytes)   inline (hot)
    

    ...и в 8u5:

     @ 16   org.sample.IntStr::inlineSideEffect (25 bytes)   force inline by CompilerOracle
       @ 4   java.lang.StringBuilder::<init> (7 bytes)   inline (hot)
         @ 3   java.lang.AbstractStringBuilder::<init> (12 bytes)   inline (hot)
           @ 1   java.lang.Object::<init> (1 bytes)   inline (hot)
       @ 18   java.lang.StringBuilder::append (8 bytes)   inline (hot)
         @ 2   java.lang.AbstractStringBuilder::append (62 bytes)   already compiled into a big method
       @ 21   java.lang.StringBuilder::toString (17 bytes)   inline (hot)
         @ 13   java.lang.String::<init> (62 bytes)   inline (hot)
           @ 1   java.lang.Object::<init> (1 bytes)   inline (hot)
           @ 55   java.util.Arrays::copyOfRange (63 bytes)   inline (hot)
             @ 54   java.lang.Math::min (11 bytes)   (intrinsic)
             @ 57   java.lang.System::arraycopy (0 bytes)   (intrinsic)
    

    вы может заметить, что версия 7u55 мельче, и похоже, что ничего не вызывается после StringBuilder методы -- это хороший признак того, что оптимизация строк действует. Действительно, если вы запустите 7u55 с -XX:-OptimizeStringConcat, снова появятся подзвонки, и производительность снизится до уровней 8u5.

  • хорошо, поэтому нам нужно выяснить, почему 8u5 не делает ту же оптимизацию. Grep http://hg.openjdk.java.net/jdk9/jdk9/hotspot для "StringBuilder", чтобы выяснить, где VM обрабатывает оптимизацию StringConcat; это приведет вас в src/share/vm/opto/stringopts.cpp

  • hg log src/share/vm/opto/stringopts.cpp чтобы выяснить последние изменения там. Одним из кандидатов будет:

    changeset:   5493:90abdd727e64
    user:        iveresov
    date:        Wed Oct 16 11:13:15 2013 -0700
    summary:     8009303: Tiered: incorrect results in VM tests stringconcat...
    
  • ищите темы обзора в списках рассылки OpenJDK (достаточно легко для Google для сводки набора изменений): http://mail.openjdk.java.net/pipermail/hotspot-compiler-dev/2013-October/012084.html

  • Spot " String concat optimization оптимизация сворачивает шаблон [...] в одно выделение строки и формирование результата непосредственно. Все возможные деопты, которые могут произойти в оптимизированном коде, перезапускают этот шаблон с самого начала (начиная с выделения StringBuffer). это означает, что вся картина должна быть побочный эффект бесплатный." Эврика?

  • выпишите контрастный эталон:

    @Fork(5)
    @Warmup(iterations = 5)
    @Measurement(iterations = 5)
    @BenchmarkMode(Mode.AverageTime)
    @OutputTimeUnit(TimeUnit.NANOSECONDS)
    @State(Scope.Benchmark)
    public class IntStr {
        private int counter;
    
        @GenerateMicroBenchmark
        public String inlineSideEffect() {
            return new StringBuilder().append(counter++).toString();
        }
    
        @GenerateMicroBenchmark
        public String spliceSideEffect() {
            int cnt = counter++;
            return new StringBuilder().append(cnt).toString();
        }
    }
    
  • измерьте его на JDK 7u55, видя ту же производительность для встроенных/сращенных побочных эффектов:

    Benchmark                       Mode   Samples         Mean   Mean error    Units
    o.s.IntStr.inlineSideEffect     avgt        25       65.460        1.747    ns/op
    o.s.IntStr.spliceSideEffect     avgt        25       64.414        1.323    ns/op
    
  • измерьте его на JDK 8u5, видя ухудшение производительности со встроенным эффектом:

    Benchmark                       Mode   Samples         Mean   Mean error    Units
    o.s.IntStr.inlineSideEffect     avgt        25       84.953        2.274    ns/op
    o.s.IntStr.spliceSideEffect     avgt        25       65.386        1.194    ns/op
    
  • отправить отчет об ошибке (https://bugs.openjdk.java.net/browse/JDK-8043677), чтобы обсудить это поведение с VM guys. Обоснование оригинального исправления является твердым камнем, однако интересно, если мы можем/должны вернуть эту оптимизацию в некоторых тривиальных случаях, подобных этим.

  • ???

  • прибыль.

и да, я должен опубликовать результаты для бенчмарка, который перемещает инкремент из StringBuilder цепи, делая это перед всей цепочкой. Также, перешли на обычное время, и Н/ОП. Это с JDK 7u55:

Benchmark                      Mode   Samples         Mean   Mean error    Units
o.s.IntStr.integerToString     avgt        25      153.805        1.093    ns/op
o.s.IntStr.stringBuilder0      avgt        25      128.284        6.797    ns/op
o.s.IntStr.stringBuilder1      avgt        25      131.524        3.116    ns/op
o.s.IntStr.stringBuilder2      avgt        25      254.384        9.204    ns/op
o.s.IntStr.stringFormat        avgt        25     2302.501      103.032    ns/op

а это 8u5:

Benchmark                      Mode   Samples         Mean   Mean error    Units
o.s.IntStr.integerToString     avgt        25      153.032        3.295    ns/op
o.s.IntStr.stringBuilder0      avgt        25      127.796        1.158    ns/op
o.s.IntStr.stringBuilder1      avgt        25      131.585        1.137    ns/op
o.s.IntStr.stringBuilder2      avgt        25      250.980        2.773    ns/op
o.s.IntStr.stringFormat        avgt        25     2123.706       25.105    ns/op

stringFormat на самом деле немного быстрее в 8u5, и все остальные тесты одинаковы. Это укрепляет гипотезу о побочном эффекте обрыва в цепях SB у главного виновника в исходном вопросе.

Я думаю, что это связано с CompileThreshold флаг, который управляет, когда байтовый код компилируется в машинный код JIT.

Oracle JDK имеет количество по умолчанию 10,000 в качестве документа на http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html.

где OpenJDK я не смог найти последний документ по этому флагу; но некоторые почтовые потоки предлагают гораздо более низкий порог: http://mail.openjdk.java.net/pipermail/hotspot-compiler-dev/2010-November/004239.html

кроме того, попробуйте включить / выключить флаги Oracle JDK, такие как -XX:+UseCompressedStrings и -XX:+OptimizeStringConcat. Я не уверен, что эти флаги включены по умолчанию на OpenJDK, хотя. Может кто-нибудь, пожалуйста, предложить.

один опыт, который вы можете сделать, - это сначала запустить программу много раз, скажем, 30 000 циклов, сделать систему.gc () а затем попробуйте посмотреть на производительность. Я верю, что они принесут тот же.

и я предполагаю, что ваши настройки GC тоже одинаковы. В противном случае вы выделяете много объектов, и GC вполне может быть основной частью вашего времени выполнения.

Comments

    Ничего не найдено.