Java8時代の文字列連結変態さんまとめ

いろいろな文字列連結のコードを書いた人がいたのでまとめておきますね。
主に変態さん。


とりあえず、基準として、メモリ確保したStringBuilder版

    public static String stringBuilderJoinMem(){
        StringBuilder s = new StringBuilder(9100).append("[");
        for(int i = 0; i < strarray.length; ++i){
            if(i != 0){
                s.append("],[");
            }
            s.append(strarray[i]);
        }
        s.append("]");
        return s.toString();
    }

1037ms


ということで、まずはbackpaper0さん。比較的常人のコード。
https://gist.github.com/backpaper0/10273558

    public static String stringBuilderJoinMem_1() {
        final StringBuilder s = new StringBuilder(9100).append("[");
        final char[] delimiter = "],[".toCharArray();
        for (int i = 0, length = strarray.length; i < length; ++i) {
            s.append(strarray[i]);
            s.append(delimiter);
        }
        s.delete(s.length() - delimiter.length, s.length());
        s.append("]");
        return s.toString();
    }

これは、基本的にはif文を省こうっていう作戦ですね。あとはデリミタをchar配列にしてます。
1039ms
ほぼ変わりません。if文が減ってるのになんで?って感じですね。char配列の分が遅い?とか思ったり。


ということで、普通にif文を使ってデリミタをchar配列にしただけのもので調べてみます。

    public static String stringBuilderJoinMem_2() {
        final StringBuilder s = new StringBuilder(9100).append("[");
        final char[] delimiter = "],[".toCharArray();
        for (int i = 0, length = strarray.length; i < length; ++i) {
            if(i != 0){
                s.append(delimiter);
            }
            s.append(strarray[i]);
        }
        s.append("]");
        return s.toString();
    }

1010ms
ちょっと速くなってる・・・とは言え、まあ誤差です。実行しなおすと同じくらいの差で逆転することもあります。


if文があるのに、なぜ?という感じです。
これはおそらく

for(int i = 0; i < n; ++i){
  if(i != 0){
    foo();
  }
  bar();
}

という処理が

if(n > 0){
  bar();
}
for(int i = 1; i < n; ++i){
  foo();
  bar();
}

のように最適化されて、結局同じようなコードになってしまったからではないかと思います。
こういう、コンパイラの教科書に載ってるのような最適化を、Javaがやらないわけないんじゃないかと。


これは、StringJoinerを使ったこのコードが単にStringBuilderを使った場合と比べて、内部的な処理はほとんど同じなのに遅いということからも伺えます。

    public static String stringJoiner(){
        StringJoiner sj = new StringJoiner("],[", "[", "]");
        for(int i = 0; i < strarray.length; ++i){
            sj.add(strarray[i]);
        }
        return sj.toString();
    }

ここでは、条件分岐がaddメソッドの内部に分離されるので、さきほどのような最適化が行えず、その結果遅くなるんじゃないかと考えられます。


で、次。変態さんその1。さくらばさん。ぜんぶchar配列でやっちゃえと。
https://gist.github.com/skrb/10264845

    public static String stringArrayJoining() {
        char[] dest = new char[18000];
        dest[0] = '[';
        int destOffset = 1;
        for (int i = 0; i < strarray.length; ++i) {
            if (i != 0) {
                dest[destOffset] = ']';
                destOffset++;
                dest[destOffset] = ',';
                destOffset++;
                dest[destOffset] = '[';
                destOffset++;
            }
            strarray[i].getChars(0, strarray[i].length(), dest, destOffset);
            destOffset += strarray[i].length();
        }
        dest[destOffset] = ']';
        destOffset++;
        return new String(dest, 0, destOffset);
    }

803ms
はやー!


最後。変態さんその2。aoe-tkさん。
http://d.hatena.ne.jp/aoe-tk/20140409/1397059399

弊社の某モヒカンより「性能面でセンシティブな場面で String を使うことを考えるな。CharBuffer 使え。」との言葉を賜りました

とのことで、変態さんは某モヒカンさんかもしれませんが、CharBufferを使ったものです。

    public static String charBufferJoin() {
        CharBuffer buffer = CharBuffer.allocate(7995);
        buffer.put('[');
        for (int i = 0; i < strarray.length; ++i) {
            if (i != 0) {
                buffer.put(',').put('[');
            }
            buffer.put(strarray[i]).put(']');
        }
        buffer.flip();
        return buffer.toString();
    }

780ms
おぉ!さらに速い。しかしまあ、誤差かなー。
まあ、だいたい同じくらいの速さなら、書きやすいCharBuffer使ったほうがいいかなーということで、某モヒカンさんの言葉は正しかった感じですね。


ということで文字列連結のまとめとしては、某モヒカンさんの言葉そのまま。
「性能面でセンシティブな場面で String を使うことを考えるな。CharBuffer 使え。」
ということですね。
今回はJava8あんまり関係なかった。


あと、文字列連結のパフォーマンスなんかどこで問題になるんだ、みたいなコメントあったけど、どばーっとXMLとかHTMLとか生成するときにはそこそこ問題になったりしますね。
プロファイラで計測して、ここやべーと思ったところを修正すると3秒速くなった、とかは、結構楽しいです。