Takayuki Okazaki's Weblog
ブログ: 岡崎 - Okazaki's blog
20051124 2005年 11月 24日 木曜日
パフォーマンスの迷信: "a" + "b" 対 StringBuffer("a").append("b")
English Translation: (Yahoo!) / (Google)
Javaを最初触りだしたころのAppletというと、とても重くて、遅いというイメージがあり、おそらく、今もその印象を持ったままの方は結構いらっしゃるように思います。確かに、最初のころのJavaは重くて、遅かったこともあり、その解決策としていろいろな回避策が考案されてきました。今日ご紹介するのもそのうちのひとつです。
文字列を連結する際の技として、"a" + "b" のようにプラス演算子を使うのではなく、StringBuffer("a").append("b")のようにStringBufferを使うべしというようなものがあります。本当に、"a" + "b"は遅いのでしょうか?
きちんとしたベンチマークをするのは少し骨が折れるので、今回はバイトコードから本当に遅くなりそうなのかを推測していきます。

定数文字列同士の連結

次のようなシンプルなプログラムをみてみましょう。plusメソッドは + 演算子、appendメソッドはStringBufferのappendを使用してそれぞれ、結合結果の文字列を返します。
public class A {
  public String plus() {
     return "a" + "b";
  }

  public String append() {
     return new StringBuffer("a").append("b").toString();
  }
}
これを、JDK 1.1.5でコンパイルして、逆コンパイルしてみました。逆コンパイルは次のようにコンパイルしてから、javapコマンドに -c をつければいいだけです。
 $ javac A.java
 $ javap -c A
次は逆コンパイルした結果の抜粋です。
Method java.lang.String plus()
   0 ldc #2 <String "ab">
   2 areturn

Method java.lang.String append()
   0 new #6 <Class java.lang.StringBuffer>
   3 dup
   4 ldc #1 <String "a">
   6 invokespecial #8 <Method java.lang.StringBuffer(java.lang.String)>
   9 ldc #3 <String "b">
  11 invokevirtual #9 <Method java.lang.StringBuffer append(java.lang.String)>
  14 invokevirtual #10 <Method java.lang.String toString()>
  17 areturn
Method java.lang.String plus()の部分をみると、+ 演算子で結合した場合には、定数文字列の場合はコンパイル時点で結合されていることがわかります。一方、StringBufferの処理ではソースコードの通りStringBufferをnewして、appendが呼び出され、toStringによって文字列に変換されています。
このように、定数文字列の場合にはコンパイラによる最適化が利きますから、圧倒的に + 演算子の方が効率的なようです。

文字変数同士の結合

次に、文字変数同士の結合をみてみます。ソースコードはほとんど変わりません。
public class A {
  private String a = "a";
  private String b = "b";

  public String plus() {
     return a + b;
  }

  public String append() {
     return new StringBuffer(a).append(b).toString();
  }
}
早速逆コンパイルしてみましょう。 これも同じくJDK 1.1.5でやってみます。
Method java.lang.String plus()
   0 new #6 <Class java.lang.StringBuffer>
   3 dup
   4 aload_0
   5 getfield #9 <Field java.lang.String a>
   8 invokestatic #13 <Method java.lang.String valueOf(java.lang.Object)>
  11 invokespecial #8 <Method java.lang.StringBuffer(java.lang.String)>
  14 aload_0
  15 getfield #11 <Field java.lang.String b>
  18 invokevirtual #10 <Method java.lang.StringBuffer append(java.lang.String)>
  21 invokevirtual #12 <Method java.lang.String toString()>
  24 areturn

Method java.lang.String append()
   0 new #6 <Class java.lang.StringBuffer>
   3 dup
   4 aload_0
   5 getfield #9 <Field java.lang.String a>
   8 invokespecial #8 <Method java.lang.StringBuffer(java.lang.String)>
  11 aload_0
  12 getfield #11 <Field java.lang.String b>
  15 invokevirtual #10 <Method java.lang.StringBuffer append(java.lang.String)>
  18 invokevirtual #12 <Method java.lang.String toString()>
  21 areturn
今度は、どうやら + 演算子の方がいろいろなことをやっています。+ 演算子はコンパイラによって自動的に、StringBufferのappendに置き換えられるのですがappendされる前に String.valueOf によって一度オブジェクトからStringの値として評価されるようになっており、単にStringBuffer#append を使用した場合よりもよけいな処理が増えていて、どうやらパフォーマンス上不利なようです。おそらくは、この技が生み出されたのはこういった経緯があったのでしょう。
これはJDK 1.2.2を使ってやってみましたが、同じ結果でした。次に、JDK 1.3.1でやってみると少し変わっていました。
Method java.lang.String plus()
   0 new #6 <Class java.lang.StringBuffer>
   3 dup
   4 invokespecial #7 <Method java.lang.StringBuffer()>
   7 aload_0
   8 getfield #3 <Field java.lang.String a>
  11 invokevirtual #8 <Method java.lang.StringBuffer append(java.lang.String)>
  14 aload_0
  15 getfield #5 <Field java.lang.String b>
  18 invokevirtual #8 <Method java.lang.StringBuffer append(java.lang.String)>
  21 invokevirtual #9 <Method java.lang.String toString()>
  24 areturn

Method java.lang.String append()
   0 new #6 <Class java.lang.StringBuffer>
   3 dup
   4 aload_0
   5 getfield #3 <Field java.lang.String a>
   8 invokespecial #10 <Method java.lang.StringBuffer(java.lang.String)>
  11 aload_0
  12 getfield #5 <Field java.lang.String b>
  15 invokevirtual #8 <Method java.lang.StringBuffer append(java.lang.String)>
  18 invokevirtual #9 <Method java.lang.String toString()>
  21 areturn
奇妙なString.valueOfはなくなり、StringBuffer#appendのみが使用されるようになっています。しかし、最初からStringBuffer#appendで連結している場合にはコンストラクタによって最初の文字列が設定されている分 appendの呼び出しは1回減り、依然としてStringBuffer#appendを使う方が有利なようです。これはJDK 1.4.2でも同じでした。
ではJDK 1.5ではどうでしょうか。
public java.lang.String plus();
  Code:
   0:	new	#6; //class java/lang/StringBuilder
   3:	dup
   4:	invokespecial	#7; //Method java/lang/StringBuilder."<init>":()V
   7:	aload_0
   8:	getfield	#3; //Field a:Ljava/lang/String;
   11:	invokevirtual	#8; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   14:	aload_0
   15:	getfield	#5; //Field b:Ljava/lang/String;
   18:	invokevirtual	#8; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   21:	invokevirtual	#9; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
   24:	areturn

public java.lang.String append();
  Code:
   0:	new	#10; //class java/lang/StringBuffer
   3:	dup
   4:	aload_0
   5:	getfield	#3; //Field a:Ljava/lang/String;
   8:	invokespecial	#11; //Method java/lang/StringBuffer."<init>":(Ljava/lang/String;)V
   11:	aload_0
   12:	getfield	#5; //Field b:Ljava/lang/String;
   15:	invokevirtual	#12; //Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
   18:	invokevirtual	#13; //Method java/lang/StringBuffer.toString:()Ljava/lang/String;
   21:	areturn
JDK 1.5では + 演算子を使った方はJDK 1.5より導入された新しいStringBuilderが使用されていることの違いがあることがわかります。StringBufferとStringBuilderの違いは、StringBufferがスレッドセーフであるのに対して、StringBuilderはスレッドセーフではない代わりに同期をしない分パフォーマンスがよいというところです。
これによりJDK 1.5では + 演算子と StringBuffer#appendでは状況により形勢逆転が起こる可能性があることがわかります。このように、パフォーマンスの回避策として行ってきたことは、JDKのバージョンアップなどにより、かえって非効率な処理となってしまう可能性があります。
個人的には、このような些細なパフォーマンスアップテクニックを身につけるよりは、ソースコードの可読性を向上させ、パフォーマンスチューニングはコンパイラに任せるというスタンスがやはり正しいでしょうし、特に最近はコンパイラに任せきりでも十分なような気がします(状況によりますが・・ ;-<


投稿されたコメント:

[Trackback] 文字列の連結処理をStringの 演算子で行うと、毎回インスタンスが生成されてしまい、かなりのコストがかかる。 そのため、ループの中など頻繁に結合処理が行われる場合は、StringBufferなどを使うのが推奨されているのは周知の事実。 けど、書いてみれば分かるけど、可読性という面ではイマイチなので、やはりStringで 演算子が使いたいところ。 で、<link caption='パフォーマンスの迷信: a b 対 StringBuffer(a).append(b)' trackbac...

Posted by ブログなんだよもん on 3月月 28日, 2006年 at 05:48 午後 JST #

「逆コンパイル」ではなく「逆アセンブル」、ですよね?ちょっとびっくりしました。

Posted by 碁盤鮫 on 6月月 12日, 2008年 at 03:15 午後 JST #

★ お名前を空欄にするとIPアドレスが、お名前欄に記入されます。
コメント
コメントは無効になっています。
過去の記事
« 10月 2009
    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
       
今日
Click me to subscribe このブログを購読(RSS)
検索

このブログ著者について
ソフトウエア・インフラストラクチャー・ソリューション本部のソリューション・アーキテクトでした(2008年8月退職)。 本業はSOAソリューションならびにSun Java CAPSによるソリューションのプリセールスをお手伝いするエンジニア、とJavaエバンジェリストグループに参加してセミナーに行ったり、趣味のプログラミング・ネタをこのブログで紹介したりしていました。現在は、ふらふらとwatermint.orgで活動中〜。
リンク
 
SunホットトピックPodcast - SunホットトピックPodcast
 


 

Today's Page Hits: 996