Posted at

+ は StringBuilder にコンパイルされる ~ Java と Scala の場合

More than 3 years have passed since last update.

文字列連結するときは StringBuffer を使いなさい、と私が Java を覚えたころに教えてもらいました。その後 StringBuilder が出来た頃までは記憶があって… 今日 StringBuilder 使うなんて化石人間だ、という感じのツイートを見たのでチラ裏代わりにメモ。結果、自分は化石だったということを認識することになったのですが。


Java の場合

ググッて出てきた 文字列連結と+演算子について整理しておく を読みました。ここで最初に述べられている定数の場合はコンパイル時に連結されたリテラルが生成されることは知っていました(のでスキップ)。

続いて出てくる「+演算子でもStringBuilderでもたいして変わらない状況もある」の例では、次のような文字列連結があったときに

class A {

String concat1(String s1, String s2) {
return s1 + s2;
}
String concat2(String s1, String s2) {
return new StringBuilder().append(s1).append(s2).toString();
}
}

javac A.java (手元に入っていた javac 1.8.0_51) でコンパイルして javap -v A.class で中身(concat1 と concat2 の定義)を見てみると

  java.lang.String concat1(java.lang.String, java.lang.String);

descriptor: (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
flags:
Code:
stack=2, locals=3, args_size=3
0: new #2 // class java/lang/StringBuilder
3: dup
4: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V
7: aload_1
8: invokevirtual #4 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
11: aload_2
12: invokevirtual #4 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
15: invokevirtual #5 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
18: areturn
LineNumberTable:
line 3: 0

java.lang.String concat2(java.lang.String, java.lang.String);
descriptor: (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
flags:
Code:
stack=2, locals=3, args_size=3
0: new #2 // class java/lang/StringBuilder
3: dup
4: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V
7: aload_1
8: invokevirtual #4 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
11: aload_2
12: invokevirtual #4 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
15: invokevirtual #5 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
18: areturn
LineNumberTable:
line 6: 0
}
SourceFile: "A.java"

全く同じ。 s1 + s2StringBuilder#append() を使ったコードとしてコンパイルされるんですね。

つぎに「+演算子を使用すべきでない状況」と書かれているパターン。

Java 書いて…

import java.util.List;

class B {
String concat1(List<String> xs) {
String str = "";
for (String s: xs) {
str += s;
}
return str;
}
String concat2(List<String> xs) {
StringBuilder b = new StringBuilder();
for (String s: xs) {
b.append(s);
}
return b.toString();
}
}

javap -v して…

  java.lang.String concat1(java.util.List<java.lang.String>);

descriptor: (Ljava/util/List;)Ljava/lang/String;
flags:
Code:
stack=2, locals=5, args_size=2
0: ldc #2 // String
2: astore_2
3: aload_1
4: invokeinterface #3, 1 // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
9: astore_3
10: aload_3
11: invokeinterface #4, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z
16: ifeq 53
19: aload_3
20: invokeinterface #5, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
25: checkcast #6 // class java/lang/String
28: astore 4
30: new #7 // class java/lang/StringBuilder
33: dup
34: invokespecial #8 // Method java/lang/StringBuilder."<init>":()V
37: aload_2
38: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
41: aload 4
43: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
46: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
49: astore_2
50: goto 10
53: aload_2
54: areturn
LineNumberTable:
line 4: 0
line 5: 3
line 6: 30
line 7: 50
line 8: 53
StackMapTable: number_of_entries = 2
frame_type = 253 /* append */
offset_delta = 10
locals = [ class java/lang/String, class java/util/Iterator ]
frame_type = 250 /* chop */
offset_delta = 42
Signature: #23 // (Ljava/util/List<Ljava/lang/String;>;)Ljava/lang/String;

java.lang.String concat2(java.util.List<java.lang.String>);
descriptor: (Ljava/util/List;)Ljava/lang/String;
flags:
Code:
stack=2, locals=5, args_size=2
0: new #7 // class java/lang/StringBuilder
3: dup
4: invokespecial #8 // Method java/lang/StringBuilder."<init>":()V
7: astore_2
8: aload_1
9: invokeinterface #3, 1 // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
14: astore_3
15: aload_3
16: invokeinterface #4, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z
21: ifeq 45
24: aload_3
25: invokeinterface #5, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
30: checkcast #6 // class java/lang/String
33: astore 4
35: aload_2
36: aload 4
38: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
41: pop
42: goto 15
45: aload_2
46: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
49: areturn
LineNumberTable:
line 11: 0
line 12: 8
line 13: 35
line 14: 42
line 15: 45
StackMapTable: number_of_entries = 2
frame_type = 253 /* append */
offset_delta = 15
locals = [ class java/lang/StringBuilder, class java/util/Iterator ]
frame_type = 250 /* chop */
offset_delta = 29
Signature: #23 // (Ljava/util/List<Ljava/lang/String;>;)Ljava/lang/String;
}
SourceFile: "B.java"

あー。うん。これは遅そう。 + を使った concat1() は Java で書くなら次のようなバイトコードになっています。

  String concat1(List<String> xs) {

String str = "";
for (String s: xs) {
StringBuilder b = new StringBuilder();
str = b.append(str).append(s).toString();
}
return str;
}


Scala の場合

手元に入っていた scalac 2.11.8 で実験。

class A {

def concat1(s1: String, s2: String) = s1 + s2
}

javap して

  public java.lang.String concat1(java.lang.String, java.lang.String);

descriptor: (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=3
0: new #12 // class scala/collection/mutable/StringBuilder
3: dup
4: invokespecial #16 // Method scala/collection/mutable/StringBuilder."<init>":()V
7: aload_1
8: invokevirtual #20 // Method scala/collection/mutable/StringBuilder.append:(Ljava/lang/Object;)Lscala/collection/mutable/StringBuilder;
11: aload_2
12: invokevirtual #20 // Method scala/collection/mutable/StringBuilder.append:(Ljava/lang/Object;)Lscala/collection/mutable/StringBuilder;
15: invokevirtual #24 // Method scala/collection/mutable/StringBuilder.toString:()Ljava/lang/String;
18: areturn
LocalVariableTable:
Start Length Slot Name Signature
0 19 0 this LA;
0 19 1 s1 Ljava/lang/String;
0 19 2 s2 Ljava/lang/String;
LineNumberTable:
line 2: 0

+ で連結した concat1() は次のようなバイトコードにコンパイルされています。

  def concat1(s1: String, s2: String) =

new scala.collection.mutable.StringBuilder().append(s1).append(s2)

scala.collection.mutable.StringBuilder が使われるんですねえ。 java.lang.StringBuilder の wrapper なので、java.lang.StringBuilder にコンパイルしたほうが速そうなのに… と思っていたら 2.12 で変わるし Java9 対応でまた変わる予定と教えてもらいました

コンパイラを 2.12.0-M5 に変えて再コンパイルして javap

  public java.lang.String concat1(java.lang.String, java.lang.String);

descriptor: (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=3
0: new #14 // class java/lang/StringBuilder
3: dup
4: invokespecial #18 // Method java/lang/StringBuilder."<init>":()V
7: aload_1
8: invokevirtual #22 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
11: aload_2
12: invokevirtual #22 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
15: invokevirtual #26 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
18: areturn
LocalVariableTable:
Start Length Slot Name Signature
0 19 0 this LA;
0 19 1 s1 Ljava/lang/String;
0 19 2 s2 Ljava/lang/String;
LineNumberTable:
line 2: 0
MethodParameters:
Name Flags
s1 final
s2 final

おー。javac と同じ結果になってます。

Java9対応で変わるのは invokedynamic を使うように生成バイトコードを変えておくことで、将来的な文字列連結性能向上を再コンパイルなしに可能にする (JEP 280: Indify String Concatenation) という話のようですが、あまりよく理解できてません。とはいえ javac と同じ形式で吐いておけば、JVMの最適化に乗っかりやすいというのは直感的に理解はできます。