我们说我们有一些代码如下:
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倍3倍在一个包含10,000个元素的循环中。当然,使用手册StringBuilder
始终是首选方法,但编译器肯定不会对+=
方法的性能产生负面影响吗?
答案 0 :(得分:6)
所以问题必须是 - 为什么?
目前尚不清楚为什么他们在字节码编译器中没有更好地优化它。您需要询问Oracle Java编译器团队。
一种可能的解释是,HotSpot JIT编译器中可能存在将字节码序列优化为更好的代码。 (如果你很好奇,你可以修改代码,以便它编译JIT ......然后捕获并检查本机代码。但是,你可能真的发现JIT编译器完全优化了方法体......)< / p>
另一种可能的解释是,原始的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&amp; A中的代码链接来查找生成该代码的Java字节码编译器源中的实际位置:here。
源代码中没有提示解释代码生成策略的“哑”性。
所以对你的一般性问题:
Javac的StringBuilder优化是否弊大于利?
没有
我的理解是编译器开发人员进行了广泛的基准测试,以确定(整体)StringBuilder优化是值得的。
你在一个编写得很糟糕的程序中找到了一个可以更好地优化的边缘情况(假设)。这还不足以得出优化“弊大于利”。
答案 1 :(得分:1)
FWIW,我的基准测试(毫不奇怪)将手动concat()方法比在10,000个元素的循环中使用+ =快2到3倍。
我有兴趣看到您的基准,因为我的(基于优秀的JMH线束)显示+=
略快于String.concat
。当我们每循环迭代执行三次操作(s += "re"; s += "al"; s += "ly ";
)时,+=
几乎保持高效,而String.concat
则显然会受到明显的因子影响。
我在Intel Xeon E5-2695 v2 @ 2.40GHz运行OpenJDK build 1.8.0_40-ea-b23上运行我的基准测试。有四种实现:
+=
+=
desugaring String.concat
每个实现有两个版本:正常的一个,一个在循环体中执行三个操作。
这是数字。这是吞吐量,因此越高越好。误差是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
。 commit history for this file显示这些优化日期为2009年末,作为错误JDK-6892658的一部分。
在8u5和8u40的早期访问版本之间似乎没有任何变化我运行基准测试,所以这并不能解释为什么我们得到不同的结果。 (当然,编译器中其他地方的更改也可能会改变结果。)
这是我使用java -jar benchmarks.jar -w 5s -wi 10 -r 5s -i 30 -f 1
运行的基准代码。基准测试运行的代码和完整日志为also available as a 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();
}
}