HHeLiBeXの日記 正道編

日々の記憶の記録とメモ‥

StringBuilder#appendとStringの連結(+演算子)

StringBuilderに対して複数のStringをappendする場合、その呼び出しパターンは以下のコードに示される4種類に整理できる。

public class Test {
    private String s1 = "a";
    private String s2 = "b";
    private final String S1 = "a";
    private final String S2 = "b";
    private StringBuilder sb = new StringBuilder();

    public void test1() {
        sb.append(s1 + s2);
    }
    public void test2() {
        sb.append(s1).append(s2);
    }
    public void test3() {
        sb.append(S1 + S2);
    }
    public void test4() {
        sb.append(S1).append(S2);
    }
    private Test() {
    }
}

観点は以下の2つである。

  • appendする複数のStringについて、それらを+演算子で連結してからappendするか、それとも各Stringを順にappendするか
  • appendするString(型の変数が指す文字列)がコンパイル時に確定するか否か

で、上記のコードをコンパイルし、"javap -c"してみると、以下のような結果が得られる。(Sunのjdk1.5.0_11を使用)

Compiled from "Test.java"
public class y2009.m05.d11.t001.Test extends java.lang.Object{
public void test1();
  Code:
   0:   aload_0
   1:   getfield    #20; //Field sb:Ljava/lang/StringBuilder;
   4:   new #22; //class java/lang/StringBuilder
   7:   dup
   8:   aload_0
   9:   getfield    #24; //Field s1:Ljava/lang/String;
   12:  invokestatic    #26; //Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
   15:  invokespecial   #32; //Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
   18:  aload_0
   19:  getfield    #36; //Field s2:Ljava/lang/String;
   22:  invokevirtual   #38; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   25:  invokevirtual   #42; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
   28:  invokevirtual   #38; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   31:  pop
   32:  return

public void test2();
  Code:
   0:   aload_0
   1:   getfield    #20; //Field sb:Ljava/lang/StringBuilder;
   4:   aload_0
   5:   getfield    #24; //Field s1:Ljava/lang/String;
   8:   invokevirtual   #38; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   11:  aload_0
   12:  getfield    #36; //Field s2:Ljava/lang/String;
   15:  invokevirtual   #38; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   18:  pop
   19:  return

public void test3();
  Code:
   0:   aload_0
   1:   getfield    #20; //Field sb:Ljava/lang/StringBuilder;
   4:   ldc #52; //String ab
   6:   invokevirtual   #38; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   9:   pop
   10:  return

public void test4();
  Code:
   0:   aload_0
   1:   getfield    #20; //Field sb:Ljava/lang/StringBuilder;
   4:   ldc #10; //String a
   6:   invokevirtual   #38; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   9:   ldc #13; //String b
   11:  invokevirtual   #38; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   14:  pop
   15:  return

}

test1()とtest2()、test3()とtest4()の比較をする際の観点は前者の観点、test1()とtest3()、test2()とtest4()の比較をする際の観点は後者の観点である。
そもそもこんなことを調べ始めたのは、test1()(およびtest3())のケースでは文字列連結処理の際にStringBuilderオブジェクトが余計に生成されるのではないかと考えたから。しかしよく見ると、test3()のケースではappend()メソッドに渡すものがコンパイル時に確定するため、「"a" + "b"」が「"ab"」に最適化されているのが分かる。また、test1()のケースでは、確かに文字列連結(+演算子)のためのStringBuilderがnewされている。パッと見ると、処理コストは「test1() > test2()」、「test3() < test4()」という形になりそう。
ということで、まず計ってみる。

import java.text.DecimalFormat;
import java.text.NumberFormat;

import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

public class TestStringBuilder {

    private static NumberFormat nf = new DecimalFormat("0.000");

    private static int loopCount1 = 10000;
    private static int loopCount2 = 1000;

    private String s1 = "a";
    private String s2 = "b";
    private final String S1 = "a";
    private final String S2 = "b";
    private StringBuilder sb = new StringBuilder(loopCount2 * 2);

    private long start;
    private long end;
    private String name;

    @BeforeClass
    public static void setUpBeforeClass() throws Exception {
    }

    @AfterClass
    public static void tearDownAfterClass() throws Exception {
    }

    @Before
    public void setUp() throws Exception {
        start = System.currentTimeMillis();
    }

    @After
    public void tearDown() throws Exception {
        end = System.currentTimeMillis();
        System.out.printf("%-28s %7s %7s %24s\n", name, loopCount1, loopCount2, nf.format((end - start) / 1000.0) + " sec");
    }

    @Test
    public void test1() {
        name = "test1:append(v + v)";
        for (int i = 0; i < loopCount1; ++i) {
            sb.setLength(0);
            for (int j = 0; j < loopCount2; ++j) {
                sb.append(s1 + s2);
            }
        }
    }
    @Test
    public void test2() {
        name = "test2:append(v).append(v)";
        for (int i = 0; i < loopCount1; ++i) {
            sb.setLength(0);
            for (int j = 0; j < loopCount2; ++j) {
                sb.append(s1).append(s2);
            }
        }
    }
    @Test
    public void test3() {
        name = "test3:append(c + c)";
        for (int i = 0; i < loopCount1; ++i) {
            sb.setLength(0);
            for (int j = 0; j < loopCount2; ++j) {
                sb.append(S1 + S2);
            }
        }
    }
    @Test
    public void test4() {
        name = "test4:append(c).append(c)";
        for (int i = 0; i < loopCount1; ++i) {
            sb.setLength(0);
            for (int j = 0; j < loopCount2; ++j) {
                sb.append(S1).append(S2);
            }
        }
    }
}

結果。

test1:append(v + v)            10000    1000                4.784 sec
test2:append(v).append(v)      10000    1000                1.687 sec
test3:append(c + c)            10000    1000                0.766 sec
test4:append(c).append(c)      10000    1000                1.487 sec

まぁ、やはり予想通り。("v"は"variable"、"c"は"constant"と思ってください。)
StringBuilder#append(s1 + s2) は StringBuilder#append(s1).append(s2) とした方がよさそうと考えていたが、コンパイル時にappendする文字列が確定するかしないかをちゃんと見極めないといけないということだな。

しかし、test1()とtest2()の処理速度が約3倍も違うとは‥コンパイルオプションで最適化するように指定すると違ってきたりするのかな?‥