Оптимизирует ли Javac StringBuilder больше вреда, чем пользы?

Скажем, у нас есть код, например:

public static void main(String[] args) {
    String s = "";
    for(int i=0 ; i<10000 ; i++) {
        s += "really ";
    }
    s += "long string.";
}

(Да, я знаю, что гораздо лучшая реализация будет использовать StringBuilder, но нести меня.)

Тривиально, мы могли бы ожидать, что байт-код, произведенный, будет чем-то вроде следующего:

public static void main(java.lang.String[]);
Code:
   0: ldc           #2                  // String 
   2: astore_1      
   3: iconst_0      
   4: istore_2      
   5: iload_2       
   6: sipush        10000
   9: if_icmpge     25
  12: aload_1       
  13: ldc           #3                  // String really 
  15: invokevirtual #4                  // Method java/lang/String.concat:(Ljava/lang/String;)Ljava/lang/String;
  18: astore_1      
  19: iinc          2, 1
  22: goto          5
  25: aload_1       
  26: ldc           #5                  // String long string.
  28: invokevirtual #4                  // Method java/lang/String.concat:(Ljava/lang/String;)Ljava/lang/String;
  31: astore_1      
  32: return

Однако вместо этого компилятор пытается быть более умным - вместо использования метода concat у него есть испеченная оптимизация для использования объектов StringBuilder, поэтому мы получаем следующее:

public static void main(java.lang.String[]);
Code:
   0: ldc           #2                  // String 
   2: astore_1      
   3: iconst_0      
   4: istore_2      
   5: iload_2       
   6: sipush        10000
   9: if_icmpge     38
  12: new           #3                  // class java/lang/StringBuilder
  15: dup           
  16: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
  19: aload_1       
  20: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  23: ldc           #6                  // String really 
  25: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  28: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
  31: astore_1      
  32: iinc          2, 1
  35: goto          5
  38: new           #3                  // class java/lang/StringBuilder
  41: dup           
  42: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
  45: aload_1       
  46: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  49: ldc           #8                  // String long string.
  51: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  54: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
  57: astore_1      
  58: return

Однако для меня это кажется скорее контрпродуктивным - вместо использования одного строкового построителя для всего цикла каждый создается для каждой отдельной операции конкатенации, что делает его эквивалентным следующему:

public static void main(String[] args) {
    String s = "";
    for(int i=0 ; i<10000 ; i++) {
        s = new StringBuilder().append(s).append("really ").toString();
    }
    s = new StringBuilder().append(s).append("long string.").toString();
}

Итак, вместо первоначального тривиального плохого подхода просто создав множество строковых объектов и отбросив их, компилятор создал гораздо худший подход к созданию множества объектов String, много объектов StringBuilder, вызывая больше методов и все еще бросая их всех, чтобы генерировать тот же результат, что и без этой оптимизации.

Итак, вопрос должен быть - почему? Я понимаю, что в таких случаях:

String s = getString1() + getString2() + getString3();

... компилятор создаст только один объект StringBuilder для всех трех строк, поэтому бывают случаи, когда оптимизация полезна. Тем не менее, рассмотрение байт-кода показывает, что даже разделение приведенного выше случая на следующее:

String s = getString1();
s += getString2();
s += getString3();

... означает, что мы вернулись к случаю, когда три объекта StringBuilder создаются индивидуально. Я бы понял, если это были нечетные угловые случаи, но добавление к ним строк (и в цикле) действительно довольно распространенные операции.

Конечно, было бы тривиально определять во время компиляции, если генерируемое компилятором StringBuilder только когда-либо добавленное одно значение - и, если это было так, вместо этого используйте простое выполнение concat?

Это все с 8u5 (однако, оно вернется, по крайней мере, к Java 5, вероятно, раньше.) FWIW, мои тесты (неудивительно) поставили ручной concat() подход в 2 раза быстрее, чем использование += в цикле с 10 000 элементов. Конечно, использование руководства StringBuilder всегда является предпочтительным подходом, но, безусловно, компилятор не должен отрицательно влиять на производительность подхода +=:?

Ответы

Ответ 1

Итак, вопрос должен быть - почему?

Непонятно, почему они не оптимизируют это немного лучше в компиляторе байткода. Вам нужно будет спросить команду компилятора Oracle Java.

Одно из возможных объяснений заключается в том, что в компиляторе HotSpot JIT может быть код для оптимизации последовательности байт-кода в нечто лучшее. (Если вам было интересно, вы могли бы изменить код, чтобы он скомпилировал JIT..., а затем захватил и проанализировал собственный код. Однако вы действительно можете обнаружить, что компилятор JIT полностью оптимизирует тело метода...)

Еще одно возможное объяснение заключается в том, что исходный Java-код настолько пессимист, чтобы начать с того, что они полагали, что оптимизация его не будет иметь значительного эффекта. Подумайте, что опытный программист Java написал бы это как:

public static void main(String[] args) {
    StringBuilder sb = new StringBuilder();
    for (int i=0 ; i<10000 ; i++) {
        sb.append("really ");
    }
    sb.append("long string.");
    String s = sb.toString();
}

Это будет работать быстрее на 4 порядка.


UPDATE. Я использовал ссылку кода из связанного Q & A, чтобы найти фактическое место в источнике компилятора байт-кода Java, который генерирует этот код: .

В источнике отсутствуют какие-либо намеки на объяснение "немой" -ности стратегии генерации кода.


Итак, ваш общий вопрос:

Оптимизирует ли Javac StringBuilder больше вреда, чем пользы?

Нет.

Я понимаю, что разработчики компилятора провели обширный бенчмаркинг, чтобы определить, что (в целом) оптимизация StringBuilder стоит того.

Вы обнаружили краевой регистр в плохо написанной программе, которая может быть оптимизирована лучше (это гипотеза). Этого недостаточно, чтобы завершить оптимизацию "больше вреда, чем пользы".

Ответ 2

FWIW, мои тесты (неудивительно) поставили ручной concat() в 2x3 раза быстрее, чем использование + = в цикле с 10000 элементами.

Мне интересно посмотреть ваш тест, потому что мой (основанный на отличном JMH) показывает, что += немного быстрее, чем String.concat. Когда мы выполняем три операции на итерацию цикла (s += "re"; s += "al"; s += "ly ";), += остается почти как исполнитель, а String.concat принимает очевидный коэффициент-3.

Я запустил свой тест на Intel Xeon E5-2695 v2 @2.40GHz с запуском OpenJDK build 1.8.0_40-ea-b23. Существует четыре реализации:

  • неявный, который использует +=
  • Явный, который явно создает экземпляр StringBuilder для каждой конкатенации, представляя += desugaring
  • concat, который использует String.concat
  • smart, который использует один StringBuilder, как в Stephen C answer

Существуют две версии каждой реализации: нормальная и одна, выполняющая три операции в теле цикла.

Вот цифры. Это пропускная способность, поэтому лучше. Ошибка - это граница доверительного интервала 99,9%. (Это результат по умолчанию JMH.)

Benchmark                      Mode  Cnt     Score     Error  Units
StringBuilderBench.smart      thrpt   30  5438.676 ± 352.088  ops/s
StringBuilderBench.implicit   thrpt   30    10.290 ±   0.878  ops/s
StringBuilderBench.concat     thrpt   30     9.685 ±   0.924  ops/s
StringBuilderBench.explicit   thrpt   30     9.078 ±   0.884  ops/s

StringBuilderBench.smart3     thrpt   30  3335.001 ± 115.600  ops/s
StringBuilderBench.implicit3  thrpt   30     9.303 ±   0.838  ops/s
StringBuilderBench.explicit3  thrpt   30     8.597 ±   0.237  ops/s
StringBuilderBench.concat3    thrpt   30     3.182 ±   0.228  ops/s

Интеллектуальная реализация с использованием только одного StringBuilder намного быстрее, чем ожидалось. Из оставшихся реализаций += бьет String.concat, который превосходит явное создание экземпляра StringBuilder. Они все довольно близки, учитывая ошибку.

При выполнении трех операций для каждого цикла все реализации принимают небольшой (относительный) удар, за исключением String.concat, пропускная способность которого уменьшается в 3 раза.

Эти результаты не удивляют, учитывая, что HotSpot имеет определенные оптимизации для StringBuilder (и StringBuffer) - см. src/share/vm/opto/stringopts.cpp. история фиксации для этого файла показывает эти даты оптимизации до конца 2009 года как часть ошибки JDK-6892658.

Кажется, что никаких изменений между 8u5 и ранней версией доступа 8u40 я не использовал, поэтому не объясняю, почему мы получили разные результаты. (Конечно, изменения в другом месте компилятора также могли изменить результаты.)


Вот контрольный код, который я запускал с java -jar benchmarks.jar -w 5s -wi 10 -r 5s -i 30 -f 1. Код и полный журнал эталонного запуска также доступны как Gist.

package com.jeffreybosboom.stringbuilderbench;

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;

@State(Scope.Thread)
public class StringBuilderBench {
    //promote to non-final fields to inhibit constant folding (see JMHSample_10_ConstantFold.java)
    private String really = "really ", long_string = "long string.", re = "re", al = "al", ly = "ly ";
    @Benchmark
    public String implicit() {
        String s = "";
        for (int i = 0; i < 10000; i++)
            s += really;
        s += long_string;
        return s;
    }
    @Benchmark
    public String explicit() {
        String s = "";
        for (int i = 0; i < 10000; i++)
            s = new StringBuilder().append(s).append(really).toString();
        s = new StringBuilder().append(s).append(long_string).toString();
        return s;
    }
    @Benchmark
    public String concat() {
        String s = "";
        for (int i = 0; i < 10000; i++)
            s = s.concat(really);
        s = s.concat(long_string);
        return s;
    }
    @Benchmark
    public String implicit3() {
        String s = "";
        for (int i = 0; i < 10000; i++) {
            s += re;
            s += al;
            s += ly;
        }
        s += long_string;
        return s;
    }
    @Benchmark
    public String explicit3() {
        String s = "";
        for (int i = 0; i < 10000; i++) {
            s = new StringBuilder().append(s).append(re).toString();
            s = new StringBuilder().append(s).append(al).toString();
            s = new StringBuilder().append(s).append(ly).toString();
        }
        s = new StringBuilder().append(s).append(long_string).toString();
        return s;
    }
    @Benchmark
    public String concat3() {
        String s = "";
        for (int i = 0; i < 10000; i++) {
            s = s.concat(re);
            s = s.concat(al);
            s = s.concat(ly);
        }
        s = s.concat(long_string);
        return s;
    }
    @Benchmark
    public String smart() {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 10000; i++)
            sb.append(really);
        sb.append(long_string);
        return sb.toString();
    }
    @Benchmark
    public String smart3() {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 10000; i++) {
            sb.append(re);
            sb.append(al);
            sb.append(ly);
        }
        sb.append(long_string);
        return sb.toString();
    }
}