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 areturnMethod 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: areturnJDK 1.5では + 演算子を使った方はJDK 1.5より導入された新しいStringBuilderが使用されていることの違いがあることがわかります。StringBufferとStringBuilderの違いは、StringBufferがスレッドセーフであるのに対して、StringBuilderはスレッドセーフではない代わりに同期をしない分パフォーマンスがよいというところです。 これによりJDK 1.5では + 演算子と StringBuffer#appendでは状況により形勢逆転が起こる可能性があることがわかります。このように、パフォーマンスの回避策として行ってきたことは、JDKのバージョンアップなどにより、かえって非効率な処理となってしまう可能性があります。 個人的には、このような些細なパフォーマンスアップテクニックを身につけるよりは、ソースコードの可読性を向上させ、パフォーマンスチューニングはコンパイラに任せるというスタンスがやはり正しいでしょうし、特に最近はコンパイラに任せきりでも十分なような気がします(状況によりますが・・ ;-< |
Today's Page Hits: 45 |