Calms blog

CALMSブログ

Javaの文字列結合で+の方がStringBuilderより速かったりもする

 ※コメントでご指摘いただきましたが、コンパイラによる最適化には適用されるケースと適用されないケースがあります。

全然昔の知識ではないです。サンプルがたまたま最適化されるケースだっただけです。最適化が適用されるのはループ内とか一部だけです。ループ外にString変数を用意して、この変数に文字列結合すれば悲惨なことになります。

こちらの記事を読んで誤解して、StringBuilderを使わず被害を受けた人を何人か見かけたので、できれば下記URLなどを見て訂正をしてもらいたいところです。
http://www.pellegrino.link/2015/08/22/string-concatenation-with-java-8.html

 この点において、まるですべての状況において+結合は問題ない、と受け取られるように書いてしまっていました。迷惑をおかけした方々には深くお詫び申し上げます。

 執筆時点ではコード例のように同一スコープ内のループで文字列を組み立てる、という状況しか想定しかしていませんでしたが、明らかに考慮不足でした。大変失礼いたしました。

================================

 

 昨日、たまたま下のページを見つけました。

Javaでは文字列の連結に+演算子を使用するのを控えましょう<

 どういうサイトなのかはよく知らないのですが、+演算子による文字列結合は非効率だから、StringBufferもといStringBuilderを使いましょうという記事です。

 あー、そんな話もあったなーとか思ったのですが、よく見ると最終更新日が2013年4月とつい最近。ちょっといただけない感じですね。

 別に知らなくてもそんなに困らないと思いますが、2013年のこの時代に「+演算子の方が遅いから!」なんてどや顔で講釈たれると残念な目に遭います。実際に試してみましょう。 

 Javaは現時点で最新の1.7.0u21を使用します。

※バージョン、環境によって結果は左右します

 

 

 

 ソースコードも上記ページから拝借します。

 サンプルは1億回のループの中で文字列結合を試すものです。まず、駄目な例として紹介されている+演算子結合。

①+演算子記号

ソースコード

public class Test {
    public static void main(String[] args) {
        long startedTime = System.currentTimeMillis();
        String message = null;
        String strValue1 = "value1";
        String strValue2 = "value2";
        String strValue3 = "value3";
        String strValue4 = "value4";
        String strValue5 = "value5";
        for(int i = 0; i < 100000000; i++) {
            message = strValue1 + strValue2;
            message = message + strValue3;
            message = message + strValue4;
            message = message + strValue5;
        }
        long endedTime = System.currentTimeMillis();
        
        System.out.println("所要時間(ms):" + (endedTime - startedTime));
    }
}

・実行結果

所要時間(ms):4583 

 

 結果は約4.6秒ですね。

 次です。 

②StringBuilder

ソースコード

public class Test {
    public static void main(String[] args) {
        long startedTime = System.currentTimeMillis();
        String message = null;
        String strValue1 = "value1";
        String strValue2 = "value2";
        String strValue3 = "value3";
        String strValue4 = "value4";
        String strValue5 = "value5";
        for(int i = 0; i < 100000000; i++) {
            StringBuilder buffer = new StringBuilder();
            buffer.append(strValue1);
            buffer.append(strValue2);
            buffer.append(strValue3);
            buffer.append(strValue4);
            buffer.append(strValue5);
            message = buffer.toString();
        }
        long endedTime = System.currentTimeMillis();
        
        System.out.println("所要時間(ms):" + (endedTime - startedTime));
    

} 

・実行結果

所要時間(ms):9843

 

 StringBuilderの初期化は新規インスタンスを作るのではなく、setLengthで0を設定した方が効率的です。

 

ソースコード(setLengthに修正)

 

public class Test {
    public static void main(String[] args) {
        long startedTime = System.currentTimeMillis();
        String message = null;
        String strValue1 = "value1";
        String strValue2 = "value2";
        String strValue3 = "value3";
        String strValue4 = "value4";
        String strValue5 = "value5";
        StringBuilder buffer = new StringBuilder();
        for(int i = 0; i < 100000000; i++) {
            buffer.setLength(0);
            buffer.append(strValue1);
            buffer.append(strValue2);
            buffer.append(strValue3);
            buffer.append(strValue4);
            buffer.append(strValue5);
            message = buffer.toString();
        }
        long endedTime = System.currentTimeMillis();
        
        System.out.println("所要時間(ms):" + (endedTime - startedTime));
    }
}

 

・実行結果

所要時間(ms):6374

 

 約6.4秒。+演算子が4.6秒だったことを考えると、setLengthにしてもまだ遅いです。

 さて、上のページでも解説されていますが、普通に考えれば+演算子結合の方が都度Stringオブジェクトが生成されて非効率的なはずです。では、なぜ+演算子の方が速いのでしょうか?

 

 こういう時は我らが javap を使います。クラスファイルの中を覗いてみましょう。

 

 まずは②StringBuilderさん。javap -c Test.class を実行してみます。

 

Compiled from "Test.java"

public class Test {

  public Test();

    Code:

       0: aload_0       

       1: invokespecial #1                  // Method java/lang/Object."<init>":()V

       4: return        

 

  public static void main(java.lang.String[]);

    Code:

       0: invokestatic  #2                  // Method java/lang/System.currentTimeMillis:()J

       3: lstore_1      

       4: aconst_null   

       5: astore_3      

       6: ldc           #3                  // String value1

       8: astore        4

      10: ldc           #4                  // String value2

      12: astore        5

      14: ldc           #5                  // String value3

      16: astore        6

      18: ldc           #6                  // String value4

      20: astore        7

      22: ldc           #7                  // String value5

      24: astore        8

      26: new           #8                  // class java/lang/StringBuilder

      29: dup           

      30: invokespecial #9                  // Method java/lang/StringBuilder."<init>":()V

      33: astore        9

      35: iconst_0      

      36: istore        10

      38: iload         10

      40: ldc           #10                 // int 100000000

      42: if_icmpge     103

      45: aload         9

      47: iconst_0      

      48: invokevirtual #11                 // Method java/lang/StringBuilder.setLength:(I)V

      51: aload         9

      53: aload         4

      55: invokevirtual #12                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;

      58: pop           

      59: aload         9

      61: aload         5

      63: invokevirtual #12                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;

      66: pop           

      67: aload         9

      69: aload         6

      71: invokevirtual #12                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;

      74: pop           

      75: aload         9

      77: aload         7

      79: invokevirtual #12                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;

      82: pop           

      83: aload         9

      85: aload         8

      87: invokevirtual #12                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;

      90: pop           

      91: aload         9

      93: invokevirtual #13                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;

      96: astore_3      

      97: iinc          10, 1

     100: goto          38

     103: invokestatic  #2                  // Method java/lang/System.currentTimeMillis:()J

     106: lstore        10

     108: getstatic     #14                 // Field java/lang/System.out:Ljava/io/PrintStream;

     111: new           #8                  // class java/lang/StringBuilder

     114: dup           

     115: invokespecial #9                  // Method java/lang/StringBuilder."<init>":()V

     118: ldc           #15                 // String 所要時間(ms):

     120: invokevirtual #12                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;

     123: lload         10

     125: lload_1       

     126: lsub          

     127: invokevirtual #16                 // Method java/lang/StringBuilder.append:(J)Ljava/lang/StringBuilder;

     130: invokevirtual #13                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;

     133: invokevirtual #17                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V

     136: return        

}

 と、長々みてもわかりにくいので、

           message = strValue1 + strValue2

 の部分だけ抜き出します。

 Builderの場合は

            buffer.append(strValue2);

 ですね。

 それがこの辺。

      51: aload         9

      53: aload         4

      55: invokevirtual #12                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;

      58: pop           

      59: aload         9

      61: aload         5

 

 55:を見るとStringBuilderのappendが呼び出されるのがわかります。

 で、次は①+演算子の該当箇所。 

      36: new           #9                  // class java/lang/StringBuilder

      39: dup           

      40: invokespecial #10                 // Method java/lang/StringBuilder."<init>":()V

      43: aload         4

      45: invokevirtual #11                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;

      48: aload         5

      50: invokevirtual #11                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;

      53: invokevirtual #12                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;

 +演算子を使ったはずなのに、StringBuilderのインスタンスを作ってappendを2回使って最後にtoStringしています。

 実はこのように、最新のJavaコンパイラは+演算子をStringBuilderに最適化してくれるんです。つまり、①+演算子コンパイルした場合、実際に実行されるソースはこんな感じのものになります。 

 publicclassTest{

    public static void main(String[] args) {
        long startedTime = System.currentTimeMillis();
        String message = null;
        String strValue1 = "value1";
        String strValue2 = "value2";
        String strValue3 = "value3";
        String strValue4 = "value4";
        String strValue5 = "value5";
        for(int i = 0; i < 100000000; i++) {
            message = new StringBuilder().append(strValue1).append(strValue2).toString();
            message = new StringBuilder().append(message).append(strValue3).toString();
            message = new StringBuilder().append(message).append(strValue4).toString();
            message = new StringBuilder().append(message).append(strValue5).toString();
        }
        long endedTime = System.currentTimeMillis();
        
        System.out.println("所要時間(ms):" + (endedTime - startedTime));
    }
}

所要時間(ms):4562 

 実行結果が約4.6秒と、①+演算子の結果と変わらないことが確認できました。

  つまり+演算子を使っているつもりが、内部的にはStringBuilderになっていたんです。

 ついでにこの結果から、少なくともこのバージョンでは②StringBuilderのように単一のStringBuilderで1個ずつappendするより、使い捨てのStringBuilderでappendしてtoStringする方が速いことがわかりました。だからってわざわざこうしようとは思いませんが。

 

 

 基本的にStringBuilderを使った方が良いことに間違いはないです(メンテナンス性とか考えると)。でも、+演算子の文字列結合を悪者扱いする必要もないということです。

 こういう「ちょっと昔は駄目だったもの」が意外と改善されていたりするので、古い知識でモノを語るのは注意が必要ですね。

 

 

 ちなみに上のサンプル、こうするとさらに速くなります。

public class Test {
    public static void main(String[] args) {
        long startedTime = System.currentTimeMillis();
        String message = null;
        final String strValue1 = "value1";
        final String strValue2 = "value2";
        final String strValue3 = "value3";
        final String strValue4 = "value4";
        final String strValue5 = "value5";
        for(int i = 0; i < 100000000; i++) {
            message = new StringBuilder().append(strValue1).append(strValue2).toString();
            message = new StringBuilder().append(message).append(strValue3).toString();
            message = new StringBuilder().append(message).append(strValue4).toString();
            message = new StringBuilder().append(message).append(strValue5).toString();
        }
        long endedTime = System.currentTimeMillis();
        
        System.out.println("所要時間(ms):" + (endedTime - startedTime));
    }
}

所要時間(ms):3048

 

 そして言うまでもないけどこれが最強。

public class Test {
    public static void main(String[] args) {
        long startedTime = System.currentTimeMillis();
        String message = null;
        final String strValue1 = "value1";
        final String strValue2 = "value2";
        final String strValue3 = "value3";
        final String strValue4 = "value4";
        final String strValue5 = "value5";
        for(int i = 0; i < 100000000; i++) {
            message = strValue1 + strValue2 + strValue3 + strValue4 + strValue5;
        }
        long endedTime = System.currentTimeMillis();
        
        System.out.println("所要時間(ms):" + (endedTime - startedTime));
    }
}

所要時間(ms):1

 

 

 


 関連エントリ:

 Javaプログラマ必須 - 対話型にJavaを実行できる「Java REPL」 - Calms blog