【发布时间】:2014-05-29 13:54:29
【问题描述】:
纯粹出于兴趣,我一直在研究 Oracle Java 编译器如何处理 String 连接,我看到了一些我没想到的东西。
给定以下代码:
public class StringTest {
public static void main(String... args) {
String s = "Test" + getSpace() + "String.";
System.out.println(s.toString());
}
// Stops the compiler optimising the concatenations down to a
// single string literal.
static String getSpace() {
return " ";
}
}
我预计编译器会将其优化为相当于:
String s = new StringBuilder("Test").append(getSpace())
.append("String.").toString();
但它实际上编译为:
String s = new StringBuilder().append("Test").append(getSpace())
.append("String.").toString();
我正在使用 32 位 jdk1.7.0_55 版本编译它。这是javap -v -l的输出:
public class StringTest
SourceFile: "StringTest.java"
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #14.#25 // java/lang/Object."<init>":()V
#2 = Class #26 // java/lang/StringBuilder
#3 = Methodref #2.#25 // java/lang/StringBuilder."<init>":()V
#4 = String #27 // Test
#5 = Methodref #2.#28 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#6 = Methodref #13.#29 // StringTest.getSpace:()Ljava/lang/String;
#7 = String #30 // String.
#8 = Methodref #2.#31 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#9 = Fieldref #32.#33 // java/lang/System.out:Ljava/io/PrintStream;
#10 = Methodref #34.#31 // java/lang/String.toString:()Ljava/lang/String;
#11 = Methodref #35.#36 // java/io/PrintStream.println:(Ljava/lang/String;)V
#12 = String #37 //
#13 = Class #38 // StringTest
#14 = Class #39 // java/lang/Object
#15 = Utf8 <init>
#16 = Utf8 ()V
#17 = Utf8 Code
#18 = Utf8 LineNumberTable
#19 = Utf8 main
#20 = Utf8 ([Ljava/lang/String;)V
#21 = Utf8 getSpace
#22 = Utf8 ()Ljava/lang/String;
#23 = Utf8 SourceFile
#24 = Utf8 StringTest.java
#25 = NameAndType #15:#16 // "<init>":()V
#26 = Utf8 java/lang/StringBuilder
#27 = Utf8 Test
#28 = NameAndType #40:#41 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#29 = NameAndType #21:#22 // getSpace:()Ljava/lang/String;
#30 = Utf8 String.
#31 = NameAndType #42:#22 // toString:()Ljava/lang/String;
#32 = Class #43 // java/lang/System
#33 = NameAndType #44:#45 // out:Ljava/io/PrintStream;
#34 = Class #46 // java/lang/String
#35 = Class #47 // java/io/PrintStream
#36 = NameAndType #48:#49 // println:(Ljava/lang/String;)V
#37 = Utf8
#38 = Utf8 StringTest
#39 = Utf8 java/lang/Object
#40 = Utf8 append
#41 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#42 = Utf8 toString
#43 = Utf8 java/lang/System
#44 = Utf8 out
#45 = Utf8 Ljava/io/PrintStream;
#46 = Utf8 java/lang/String
#47 = Utf8 java/io/PrintStream
#48 = Utf8 println
#49 = Utf8 (Ljava/lang/String;)V
{
public StringTest();
flags: ACC_PUBLIC
LineNumberTable:
line 2: 0
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 2: 0
public static void main(java.lang.String...);
flags: ACC_PUBLIC, ACC_STATIC, ACC_VARARGS
LineNumberTable:
line 4: 0
line 5: 27
line 6: 37
Code:
stack=2, locals=2, args_size=1
0: new #2 // class java/lang/StringBuilder
3: dup
4: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V
7: ldc #4 // String Test
9: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
12: invokestatic #6 // Method getSpace:()Ljava/lang/String;
15: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
18: ldc #7 // String String.
20: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
23: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
26: astore_1
27: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
30: aload_1
31: invokevirtual #10 // Method java/lang/String.toString:()Ljava/lang/String;
34: invokevirtual #11 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
37: return
LineNumberTable:
line 4: 0
line 5: 27
line 6: 37
static java.lang.String getSpace();
flags: ACC_STATIC
LineNumberTable:
line 10: 0
Code:
stack=1, locals=0, args_size=0
0: ldc #12 // String
2: areturn
LineNumberTable:
line 10: 0
}
有趣的是,我读过here,ECJ 编译器确实实际上编译为参数构造函数(虽然我还没有为自己验证过),所以我的问题是 为什么 Oracle 的编译器不做同样的优化?
基于 cmets,我使用更长的 String 运行了另一个测试,以便立即超过 StringBuilder 的支持 char[] 的默认长度:
public class StringTest {
public static void main(String... args) {
String s = "Testing a much, much longer " + getSpace() + "String.";
System.out.println(s.toString());
}
// Stops the compiler optimising the concatenations down to a single string literal
static String getSpace() {
return " ";
}
}
除了字面量的内容略有不同之外,生成的字节码完全相同,仍然使用无参数构造函数来实例化StringBuilder,然后再附加到它。在这种情况下,据我所知,代码的参数构造函数版本应该胜过无参数版本。这是因为需要在第一次调用append() 时重新调整支持char[] 的大小,然后可能需要在下一个append() 上再次执行此操作,如果附加了@987654336 @ 特别大。
根据 AnubianNoob 的建议,我对 System.arraycopy(...) 进行了快速性能测试,看看它是否确实针对空数组进行了优化。这是使用的代码:
public class ArrayCopyTest {
public static void main(String... args) {
char[] array = new char[16];
final long test1Start = System.nanoTime();
for (int i = 0; i < 1000000; i++) {
System.arraycopy(array, 0, array, 0, array.length);
}
final long test1End = System.nanoTime();
System.out.println("Elapsed Time (empty array copies)");
System.out.println("=================================");
System.out.println((test1End - test1Start) + "ns");
char[] array2 = new char[] {'0', '1', '2', '3', '4', '5', '6', '7', '8',
'9', 'a', 'b', 'c', 'd', 'e', 'f'};
final long test2Start = System.nanoTime();
for (int i = 0; i < 1000000; i++) {
System.arraycopy(array2, 0, array2, 0, array2.length);
}
final long test2End = System.nanoTime();
System.out.println("Elapsed Time (non-empty array copies)");
System.out.println("=====================================");
System.out.println((test2End - test2Start) + "ns");
}
}
在具有 i7-2600 CPU @ 3.40 GHz 3.39 GHz 和 3.24 GB 可用 RAM 的 Windows 7.1 32 位机器上运行:
Elapsed Time (empty array copies)
=================================
26660199ns
Elapsed Time (non-empty array copies)
=====================================
19431962ns
为了确定,我跑了大约五次。实际上,当数组不为空时,它似乎在超过一百万次迭代时表现更好。正如 Mike Strobel 正确指出的那样,以上并不是一个有意义的基准。
【问题讨论】:
-
您为什么认为这是一种优化?两者唯一的区别是容量,使用基于字符串的构造函数时容量更大。
-
@Duncan 我并不是说一个版本一定比另一个更好(我还没有深入研究以形成一个明智的意见),我只是想知道为什么 Oracle当 ECJ 更喜欢有参数的构造函数时,更喜欢无参数的构造函数。如果我要手动编写代码,我会使用带参数的构造函数,所以我(也许天真地)假设编译器也会这样做。
-
@Duncan 仔细观察后,如果附加到
StringBuilder的初始String长度超过 16 个字符,那么后面的char[]将立即扩展为至少 34,如果String少于34 个字符,则有一些备用容量。如果它长于 34 个字符,那么新数组将恰好足够长以适应新内容,这意味着对append()的下一次调用将再次导致数组调整大小。鉴于此,我认为参数化构造函数将是编译器选择的更好选择... -
...因为它确保它足够大以容纳最初的
String,并在最后剩余 16 个字符的备用容量。 -
恐怕这不是一个有意义的基准。使用 Java 进行基准测试是一项棘手的工作,您必须考虑诸如延迟 JIT 之类的问题。如果第一个测试用例一直运行得较慢,我敢打赌你吃午饭,如果你交换测试的顺序,结果会改变:)。
标签: java javac bytecode compiler-optimization