Stringの結合ふたたび

id:JavaBlack:20050323#p1の続き.
次のような例について考える.maxは決して小さくなく,パフォーマンス的に無視できないと仮定する.

例1:
public StringBuffer method1(int max){
  StringBuffer str = new StringBuffer() ; //ローカル変数
  for( i=0 ; i< max ; i++){
      str.append( i ).append( "文字列1");
  }
  str.append("文字列2");
  return str;
}
例2
StringBuffer str = new StringBuffer() ; // インスタンス変数
public StringBuffer method1(int max){
  for( i=0 ; i< max; i++){
      str.append(i).append("文字列1");
  }
  str.append("文字列2");
  return str;
}

これらでは,パフォーマンスが異なる可能性がある.

「フィールド変数よりメソッド内ローカルな変数の方が早い」という怪しげな「最適化」もかつてはあった.単純なインタプリタ型のVMの頃ならばそういうこともあっただろう.しかし動的最適化を利用するようになってからは誤差程度の違いしかなく,今や既に「時代遅れとなったチューニング」の域に入っている.
問題なのはStringBufferを使っていることだ.StringBuufferはそれ自体が同期化されている.volatile変数やSynchronizedメソッドはそれ自体にreordering*1に制約を加え,高度な最適化を阻害するという性質がある*2 *3.JDK1.5以後のStringBuilderでは同期化されていないので,StringBufferではなくStringBuilderを使った場合はこのような問題は起こらない.この問題はVector/HashtableとArrayList/HashMapでもまったく同じことが言える.

それでも例1のように単純にメソッド内ローカルな変数である限りは他スレッドと競合する可能性はなく,VMがそれを検出して自動的に冗長で無意味な同期を除去してくれる「かもしれない」.これに比べれば例2の場合は他スレッドとの競合がないことを検出する必要があり,最適化が数段難しくなる.
これを回避するには,次のようにVMが解析しやすいように一時的な変数を使用すると言う手が考えられる.

例3
StringBuffer str = new StringBuffer ; // インスタンス変数
public StringBuffer method1(int max){
 StringBuffer temp = new StringBuffer();
  for( i=0 ; i< max; i++){
      temp.append( i ).append("文字列1");
  }
  temp.append("文字列2");

  str.append( temp );
  return str;
}

ただし,これはお世辞にもきれいな方法ではないし,これによってパフォーマンスが向上する保証もないので諸手を挙げて賛成できるわけではない.あくまで最終手段と考えておくべきだ.もちろんJDK1.5以後は「StringBufferを使わない.StringBuilderを使え.」が推奨される書き方である.

*1:命令の処理順序を変更するCPU/アセンブラレベルの最適化手法.逐次プログラムにおいては最終的な処理結果が変わらない範囲で自由に変更しても構わないが,並列プログラムにおいてはごくわずかな変更でさえも処理結果に影響を与える可能性があり簡単には変更できない.詳しくはJMM参照.

*2:http://www.netgene.co.jp/java/concurrentTips.html#synchronizedMethod

*3:http://gee.cs.oswego.edu/dl/jmm/cookbook.html