Чому StringBuilder # додається (int) швидше в Java 7, ніж у Java 8?


76

Досліджуючи невеликі дебати з використанням "" + nі Integer.toString(int)для перетворення цілочисеного примітиву у рядок, я написав цей мікровивідний знак JMH:

@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 (сучасний 64-розрядний Mageia 4, процесор Intel i7-3770, 32 ГБ оперативної пам'яті). Першим JVM був той, який постачався з Oracle JDK 8u5 64-bit:

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)( stringBuilder2мікровизначення), здається, не швидше.

Чи є ця розбіжність у роботі проблемою з тестовим джгутом? Або мій OpenJDK JVM містить оптимізацію, яка вплине на цей конкретний (анти) шаблон?

РЕДАГУВАТИ:

Для більш прямого порівняння я встановив 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 мала більш агресивну оптимізацію рядків?

РЕДАКТУВАТИ 2 :

Для повноти, ось параметри віртуальної машини, пов’язані із рядками, для обох цих JVM:

Для 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}   

UseStringCacheВаріант був видалений в Java 8, без заміни, так що я сумніваюся , що робить ніякої різниці. Інші параметри мають однакові налаштування.

EDIT 3:

Бок про бік порівняння вихідного коду з AbstractStringBuilder, StringBuilderі Integerкласів з src.zipфайлу показує нічого noteworty. Окрім цілого ряду косметичних змін та змін у документації, Integerтепер є певна підтримка цілих чисел без підпису та StringBuilderбула трохи реконструйована для надання більше коду StringBuffer. Здається, жодна з цих змін не впливає на шляхи коду StringBuilder#append(int), якими я користувався, хоча я, можливо, щось пропустив.

Порівняння коду збірки, створеного для, IntStr#integerToString()і IntStr#stringBuilder0()є набагато цікавішим. Базовий макет коду, сформованого для, IntStr#integerToString()був подібним для обох JVM, хоча 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, повинен відповісти на ці питання ...


Ви не мали на увазі: new StringBuilder().append(this.counter++).toString();і третій тест з return "" + this.counter++;?
assylias

4
@assylias: stringBuilderМетод перекладається точно в той самий байт-код, що і return "" + this.counter++;.
Побачу

@assylias: ось ти. Жодної реальної різниці я не бачу ...
thkala

Ви можете додати тест String.format("%d",n);також

1
@JarrodRoberson: як щодо цього? String.format("%d",n)приблизно на порядок повільніше , ніж все ...
thkala

Відповіді:


97

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", щоб з'ясувати, де ВМ обробляє оптимізацію 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

  • Пляма "Оптимізація рядка concat оптимізує шаблон [...] в одне виділення рядка і безпосередньо формує результат. Усі можливі запуски, що можуть трапитися в оптимізованому коді, перезапускають цей шаблон із самого початку (починаючи з розподілу 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 ), щоб обговорити цю поведінку з хлопцями з ВМ. Обґрунтування оригінального виправлення є твердим, цікаво, проте, якщо ми можемо / повинні повернути цю оптимізацію в таких тривіальних випадках, як ці.

  • ???

  • ПРИБУТОК.

І так, я повинен опублікувати результати для еталону, який рухає приріст від StringBuilderланцюга, роблячи це перед усім ланцюгом. Крім того, переключено на середній час і ns / op. Це 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, головним винуватцем оригінального питання.


1
Дуже мило зроблено! Це одне тонке маленьке бі ... помилкове ... питання - не зовсім те, на що звикли очікувати більшість програмістів Java. Я знайшов кілька посилань на оптимізацію рядків wrt, які мали проблеми з коректністю, тому я мав свої підозри, але не мав часу це зафіксувати. Я також ціную звіт про помилку, навіть якщо він нікуди не дівається.
thkala

1
О, я також підтвердив ваші висновки, перемістивши збільшення лічильника перед StringBuilderдзвінками та порівняльним тестом. Цікаво, які ще маленькі перлини цього типу можуть бути ...
thkala

5

Я думаю, це пов'язано з CompileThresholdпрапором, який контролює, коли код байту компілюється в машинний код за допомогою JIT.

За замовчуванням Oracle JDK нараховує 10000 як документ за адресою 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 циклів, виконати System.gc (), а потім спробувати поглянути на продуктивність. Я вірю, що вони дали б те саме.

І я припускаю, що ваші налаштування GC теж однакові. В іншому випадку ви виділяєте багато об’єктів, і GC цілком може бути основною частиною вашого часу роботи.


6
JMH за замовчуванням виконує 20 ітерацій розминки, кожна з яких містить кілька мільйонів викликів методів мікровизначення в цьому випадку. Теоретично CompileThreshold це мало мати великого ефекту ...
thkala

@thkala Мені цікаво, який результат, якщо ОП спробує розігрітись тут. Але я згоден з вами, що його код занадто простий для великої кімнати вдосконалення. Також те, що деякі JDK замінює загальний основний код продуктивності, тобто код із рядковими операціями, власним кодом. Не дуже впевнений у реалізації OpenJDK.
Alex Suo,

Вибачте, я просто зрозумів, що ви ОП :)
Алекс Суо,

Здається, це більше проблема Java7 / Java8, ніж проблема OpenJDK / HotSpot - я додав орієнтир на Oracle JDK 7u55 ...
thkala

Здається, параметри віртуальної машини, пов’язані зі рядками, однакові в обох версіях. Тим НЕ менше, Java 8 має інший механізм GC ...
thkala
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.