Оптимизирует ли 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();
}
}