LoginSignup
10
9

More than 5 years have passed since last update.

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

Posted at

文字列連結するときは 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の最適化に乗っかりやすいというのは直感的に理解はできます。

10
9
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
9