Javaの文字列結合パフォーマンス その2

さて、以下のようなコードではどちらのほうが処理が速くなるだろうか。

1. +演算子で結合
String s = s1 + s2 + s3 + s4 + s5 + s6 + s7 + s8 + s9;

2. StringBuilderで結合
StringBuilder sb = new StringBuilder();
sb.append(s1);
sb.append(s2);
sb.append(s3);
sb.append(s4);
sb.append(s5);
sb.append(s6);
sb.append(s7);
sb.append(s8);
sb.append(s9);
String s = sb.toString();

Core i5 1.6GHz環境で実行したところ、1m秒あたりの処理回数は以下となった。

1. +演算子で結合
15000 ops/msec
2. StringBuilderで結合
 7000 ops/msec

+演算子のほうが倍以上速いことになる。
なぜ、このような差が出てしまったのだろうか。
String s = s1 + s2 + s3 + s4 + s5 + s6 + s7 + s8 + s9;
をコンパイルした結果を擬似的にJavaのコードで表すと以下になる。
String s = (new StringBuilder()).append(s1).append(s2).append(s3)
                                .append(s4).append(s5).append(s6)
                                .append(s7).append(s8).append(s9)
                                .toString();
これと2. StringBuilderで結合との違いはsb変数がappendメソッド呼び出し時にいちいち登場しないところにある。
これはappendメソッドが戻り値として、呼び出し先のオブジェクト参照を返すことをうまく利用している。
毎回、sb変数を参照する場合、 メソッド呼び出しを実行する前にオペランドスタックに毎回、sb変数をプッシュする必要が生じてしまう。

というわけで以下の書き方をすると、+演算子で結合した場合と同じ処理速度になる。

3. StringBuilderで結合改良
StringBuilder sb = new StringBuilder();
sb.append(s1)
  .append(s2)
  .append(s3)
  .append(s4)
  .append(s5)
  .append(s6)
  .append(s7)
  .append(s8)
  .append(s9);
String s = sb.toString();

処理速度とコードの見通しを考えると+演算子で結合したほうがよさそうだ。

Javaの文字列結合パフォーマンス その1

よくJavaでは文字列結合で+演算子より、StringBuilderを使ったほうがパフォーマンスがいいと言われるけれど、それはこういうケースだろう。
String s = "";
for (String e : stringList) {
    s += e;
}
ループ中で文字列を次々に連結させていく操作だ。
この場合はループの外でStringBuilderインスタンスを一度だけ生成して使ったほうが効率がいい。
StringBuilder sb = new StringBuilder();
for (String e : stringList) {
    sb.append(e);
}
String s = sb.toString();
ちなみにJava8ではStringにjoinメソッドが追加されたので、そちらを使うのが便利。

しかしながら、定数文字列と変数から文字列を作り出す場合にも同じようにStringBuilderを使っているケースが見られる。
StringBuilder sb = new StringBuilder();
sb.append("Hello, ");
sb.append(name);
sb.append("!");
String s = sb.toString();
この場合は+演算子で結合したほうが見た目はわかりやすい。
String s = "Hello, " + name + "!";
しかし、パフォーマンス的にはStringBuilderでappendしたほうが速い...のだろうか?

シェルスクリプトで複数データを一括UPDATEする

さて、どうして"行区切りのデータを適当な行数でまとめる"なんてことがしたかったというと
シェルスクリプトでRDBのデータを複数行更新する必要があったから。

11111
22222
33333
44444
...

のようなデータがあった時にこれらをキーにステータスを更新したいとしよう。

UPDATE Foo
   SET Status = '3'
 WHERE ID = '11111';

このUPDATE文を行数分発行するのはあまりやりたくないだろう。
なので、こうする。

UPDATE Foo
   SET Status = '3'
 WHERE ID IN('11111', '22222', '33333');

この時に元のデータから '11111', '22222', '33333' という文字列を作りたかったのだ。

実際にはSQL文の長さ上限とか、コミットサイズとかの制限があるので、
適当なデータ数を決めて、UPDATE文を作成すればいい。

行区切りのデータを適当な行数でまとめる

文で説明しようとすると難しいけど、

$ cat paste.dat
1
2
3
4
5
6
7
8
9

のようなデータがあった時に

1,2,3
4,5,6
7,8,9

にしたい。
これはpasteコマンドを使う。

$ paste -d ',' - - - < paste.dat | sed -e 's/,*$//'

なんでpasteコマンドでこんなことができるかというと
引数として-(ハイフン)を渡すと標準入力から読み込むから。
しかも、-(ハイフン)を書いた回数分、標準入力から読み込むので、
1行目、2行目、3行目を別に読み込んで、くっつけてくれる。

最後にちょうど3の倍数行で終わらなかった場合のために行末の,(カンマ)を削除する。

前回の文字列を繰り返す方法と組み合わせると以下のようになる。

$ paste -d ',' $(printf "%3s" | sed -e 's/ / -/g') < past.dat | sed -e 's/,*$//'

これなら100行毎とかでも対応できる。

シェルで文字列を繰り返す

シェルスクリプトでループを書くのはあまりかっこよくないなと。
ポイントはprintfでスペースを繰り返すところ。

$ printf "%3s" | sed -e 's/ /foo/g'
foofoofoo