【发布时间】:2014-08-28 04:21:34
【问题描述】:
假设我们有如下代码:
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() 方法比在循环中使用 += 快 2x3 倍10,000 个元素。当然,使用手动StringBuilder 总是更好的方法,但编译器肯定也不应该对+= 方法的性能产生不利影响吗?
【问题讨论】:
-
你能链接到它被称为优化的地方吗?
-
@SotiriosDelimanolis 当然:docs.oracle.com/javase/specs/jls/se5.0/html/…
-
我在Java 6上试了一下(第一次用javap,万岁),输出完全一样。
-
这显然不是重复的 Jarrod!至于实际问题,我想没有人会费心尝试编写一些东西来将循环中的字符串连接转换为等效的字符串构建器设置 - 我想不出任何可以阻止它的东西,但我认为我缺少一些东西这意味着如果不是这种情况,您将无法自动化它。
-
如果不是因为在字符串本质上是可变的语言中它不会是愚蠢的,我会同意。实际上,想法的表达对我来说很重要,您希望对想法的表示进行一些巧妙的解释。字符串连接非常普遍,并且与不变性的概念完全不兼容(因为你总是会得到一些新的东西,但会带来相关的性能成本),所以我们有这种折衷方案,这显然是令人困惑的。如果将 += 运算符扩展到 StringBuilders 以进行追加,则可能一切正常 - 看起来都一样。
标签: java string optimization javac stringbuilder