Performance Tuning Tips

Revised: 06th/Dec./2003; Since: Oct./14th/2003

不変オブジェクトと可変オブジェクト

オブジェクトは、不変オブジェクトと可変オブジェクトに分けられます。

可変オブジェクト (mutable object)
private 修飾されたメンバー変数を変更するメソッド(setter)を、外部に対するインタフェース・メソッドとして公開しているもの
不変オブジェクト (immutable object)
取得メソッド (getter) のみを公開しているもの

不変オブジェクトは、データの保護を手軽に実装する優れた設計手法です。不変オブジェクトを変更するためには、別のオブジェクトを new して、そのとき参照するコンストラクタで、変更後の値をセットする必要があります。これが一時オブジェクトを大量に生成する可能性を持ちます。

不変オブジェクトでよく例に挙がるのが、クラス java.lang.String 型のオブジェクトです。対応する可変オブジェクトとして、java.lang.StringBuffer が挙げられます。次のリストは、文字列の連結演算子 + の利用例です。String 型オブジェクト str に、文字列「こんにちは」を繰り返し連結しています。

String str= "";
for (int i = 0; i < 100; i++) {
    str += "こんにちは";
}

ステップ数を確認する例として、これをコンパイルしたクラスを逆アセンブルして、バイトコードを生成してみましょう。このコードのバイトコードは、次のようになります。上記のコードは "BytecodeDemo.java" に記述されているものとします。

javac BytecodeDemo.java
>javap -c BytecodeDemo
Compiled from "BytecodeDemo.java"
class BytecodeDemo extends java.lang.Object{
BytecodeDemo();
  Code:
   0:   aload_0
   1:   invokespecial   #1; //Method java/lang/Object."<init>":()V
   4:   return
public static void main(java.lang.String[]);
  Code:
   0:   ldc     #2; //String
   2:   astore_1
   3:   iconst_0
   4:   istore_2
   5:   iload_2
   6:   bipush  100
   8:   if_icmpge       37
   11:  new     #3; //class StringBuffer
   14:  dup
   15:  invokespecial   #4; //Method java/lang/StringBuffer."<init>":()V
   18:  aload_1
   19:  invokevirtual   #5; //Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
   22:  ldc     #6; //String こんにちは
   24:  invokevirtual   #5; //Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
   27:  invokevirtual   #7; //Method java/lang/StringBuffer.toString:()Ljava/lang/String;
   30:  astore_1
   31:  iinc    2, 1
   34:  goto    5
   37:  return
}

先述のページで、バイトコードについて、簡単に説明してあります。興味があれば、参照ください。

文字列の連結は、内部的には、java.lang.StringBuffer#append() が使われていることが分かります。StringBuffer 型オブジェクトは可変であり、あらかじめ予約されたバッファ領域で文字列を連結するので、String を使うときのように、連結のたびに一時オブジェクトが生成される愚を避けることができます。文字列は頻繁に利用するので、特例措置として、内部的にバッファを使う仕組みが備えられているのです。

ここで挙げたような、String 型オブジェクトのバッファによる自動処理は便利な仕組みなのですが、注意することもあります。次のコードのように、ステートメントが分けられた連結では、内部的に生成されるオブジェクトが増えます。

String str= "";
for (int i = 0; i < 100; i++) {
    str += "こんにちは";
    str += "さようなら";
}

これのバイトコードは次のようになります。

public static void main(java.lang.String[]);
  Code:
   0:   ldc     #2; //String
   2:   astore_1
   3:   iconst_0
   4:   istore_2
   5:   iload_2
   6:   bipush  100
   8:   if_icmpge       57
   11:  new     #3; //class StringBuffer
   14:  dup
   15:  invokespecial   #4; //Method java/lang/StringBuffer."<init>":()V
   18:  aload_1
   19:  invokevirtual   #5; //Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
   22:  ldc     #6; //String こんにちは
   24:  invokevirtual   #5; //Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
   27:  invokevirtual   #7; //Method java/lang/StringBuffer.toString:()Ljava/lang/String;
   30:  astore_1
   31:  new     #3; //class StringBuffer
   34:  dup
   35:  invokespecial   #4; //Method java/lang/StringBuffer."<init>":()V
   38:  aload_1
   39:  invokevirtual   #5; //Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
   42:  ldc     #8; //String さようなら
   44:  invokevirtual   #5; //Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
   47:  invokevirtual   #7; //Method java/lang/StringBuffer.toString:()Ljava/lang/String;
   50:  astore_1
   51:  iinc    2, 1
   54:  goto    5
   57:  return
}

別のステートメントのバイトコードは別の StringBuiffer 型オブジェクトで処理されます。これを繰り返すことで、無駄なオブジェクトの大量な生成/破棄が必要になります。そうであれば、事前に StringBuffer 型オブジェクトを明示的に作って連結すべきです。これは、自分で作ったバッファ・クラスの利用方法に通じます。次のコードは、繰り返しの前に StringBuffer 型オブジェクト strb を生成して、繰り返し内部では、strb に対する連結のメソッドで文字列連結を実装しています。連結が抜けたら、strb からもとの str に結果の文字列を受け渡し、不要になった strb には明示的に null を代入して参照を破棄しています。

String str= "";
StringBuffer strb = new StringBuffer(str);
for (int i = 0; i < 100; i++) {
    strb.append("こんにちは");
    strb.append("さようなら");
}
str = strb.toString();
strb = null;

これのバイトコードは次のようになります。

>javap -c BytecodeDemo
Compiled from "BytecodeDemo.java"
class BytecodeDemo extends java.lang.Object{
BytecodeDemo();
  Code:
   0:   aload_0
   1:   invokespecial   #1; //Method java/lang/Object."<init>":()V
   4:   return
public static void main(java.lang.String[]);
  Code:
   0:   ldc     #2; //String
   2:   astore_1
   3:   new     #3; //class StringBuffer
   6:   dup
   7:   aload_1
   8:   invokespecial   #4; //Method java/lang/StringBuffer."<init>":(Ljava/lang/String;)V
   11:  astore_2
   12:  iconst_0
   13:  istore_3
   14:  iload_3
   15:  bipush  100
   17:  if_icmpge       40
   20:  aload_2
   21:  ldc     #5; //String こんにちは
   23:  invokevirtual   #6; //Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
   26:  pop
   27:  aload_2
   28:  ldc     #7; //String さようなら
   30:  invokevirtual   #6; //Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
   33:  pop
   34:  iinc    3, 1
   37:  goto    14
   40:  aload_2
   41:  invokevirtual   #8; //Method java/lang/StringBuffer.toString:()Ljava/lang/String;
   44:  astore_1
   45:  aconst_null
   46:  astore_2
   47:  return
}

この例では、余分なオブジェクトが一個増えただけなので、変更後のリストでは、StringBuffer型オブジェクトの明示的な生成と、メソッドStringBuffer#toString()の呼び出しと合わせると、パフォーマンス上の利得が相殺してしまいます。しかし、一般に、不変オブジェクトによる一時オブジェクトの大量生成の可能性があれば、パフォーマンス上/設計上で有効です。

自分で実装した不変オブジェクトの変更の場合にも、バッファを備えた可変オブジェクトを対として作っておきましょう。

StringStringBuffer についての詳細は、本稿のコアパッケージの説明を参照くださ。

ループ処理の最適化

最適化の代表的な例が、繰り返し時の、不要な処理や参照の外出しです。繰り返し構造は、他の言語でも古くから言語仕様で実装されてきたため、チューニングのチップスは沢山あり、最適化のための模範例を提供してくれます。

最初に、作為的ではありますが、非効率的なループの例を挙げましょう。

int a = 1;
int b = 2;
int[] c = {0, 0, 0};
String[] array = new String[10000] ;
for (int i = 0 ; i < array.length ; i++) {
    c[0] += a + b;
    array[i] = (new Integer(c[0])).toString();
}

このコードの改善点は、次のように挙げられます。

無駄な処理を外出しにする
繰り返し内でaとbは不変なので、計算するコストを省けます。
配列を外出しにする
配列の要素への参照は、配列境界のチェックのためのオーバーヘッドが発生します。c[0]のような、固定された要素への参照が繰り返し現れる場合は、外出しにすることで、このオーバーヘッドを省けます
メンバー呼び出しをなくす
繰り返し終了条件で、オブジェクトのメソッドやフィールドを使うと、毎回ヒープ領域への参照が発生します。int型のローカル変数にコピーすることで、オブジェクトの参照と、そのメンバーの呼び出しのコストを省けます。
オブジェクト生成をなくす
繰り返しごとにオブジェクトを生成する負荷は馬鹿になりません。可能であれば、外出しにすべきです。

以上の項目を踏まえて、上記のコードは次のようにすることで、パフォーマンス上の利得が望めます。

int a = 1;
int b = 2;
int[] c = {0, 0, 0};
String[] array = new String[10000] ;
int s = 3;
int c0 = c[0];
int len = array.length ;
for (int i = 0 ; i < len ; i++) {
	c0 = c0 + s;
	array[i] = Integer.toString(c0);
}
c[0] = c0;

他にも、次のような点が言われています。これらは、実装に依存するため、採用するべきかどうかは微妙なところです。

繰り返しに限らず、不必要な処理を実行しない工夫は、常に意識しておく必要があります。また、このような最適化は、可読性の向上やコードの標準化の観点でも有効です。しかし、ここで挙げたコードの例のような、極めてプリミティブな場合は、古いJVMでない限り、最適化の効果は微々たるものです。わずかなパフォーマンス上の利得しか得られない場合や、設計を崩すような場合は採用しないほうが良いでしょう。実装に依存するような方法については、採用するかどうかの判断は、ターゲットとなる環境でのベンチマーク・テストの結果に依存します。

繰り返し時には、重い処理/不要な処理は外出しにできないか検討しましょう。

ループ についての詳細は、本稿の基本規則の説明を参照くださ。

キャスト

オブジェクト指向設計のポリモーフィズム(※)については、インタフェース志向で設計されたコレクション・フレームワークによって、その効果を知ることができます。オブジェクトを、LinkedList型やArrayList型で宣言するよりも、Link型で宣言したほうが、実装の切り替えに対して自由なコードとなるので優れていると言えます。

ポリモーフィズム(多態性:Polymorhpism)

一方、ポリモーフィズムを破壊して、実装固有のメソッドを使いたいこともあり、その場合は、明示的なキャストが必要となります。しかし、コンパイル時に解決されないキャストは、実行時にオーバーヘッドを必要とします。そのコストは、プリミティブ型 < クラス型 < インタフェース型の順番に高くなります。

何れにせよ、実装固有の機能が必要であれば、最初からターゲットのクラス型で宣言しておくことで、パフォーマンス上の利得が得られます。キャストが必要な場合でも、繰り返し処理やソートなどで、キャストが必要になる場合は、外出しにして、繰り返しキャストされることを避けるほうが賢明です。

次のコードは、繰り返しごとにキャストが発生しているため、望ましくありません。

int sum = 0;
if (obj instanceof MyClass) {
    for (int i = 0; i < 10; i++) {
        sum += ((MyClass)obj).getValue();
    }
}

次のようにして、一時オブジェクトを生成することで、キャストの繰り返しを避けることが有効です。

int sum = 0;
if (obj instanceof MyClass) {
    MyClass temp = (Myclass)obj;
    for (int i = 0; i < 10; i++) {
        sum += temp.getValue();
    }
}

キャストが必要とならないように、スーパークラス型で宣言されているメソッドだけを使うようにしましょう。キャストが必要となるコードであれば、最初から厳密な型で宣言しておきましょう。キャストを実行する場合は、最低限度の回数で済むように、一時オブジェクトなどを利用しましょう。

キャスト についての詳細は、本稿のオブジェクト指向の説明を参照くださ。

コア・パッケージの利用

ちょっとした機能を実装するのに、自分でロジックを書いてしまうことが良くあります。しかし、コア・パッケージで提供している機能であれば、それらを利用したほうが高速なことがあります。

JVM内部では、コア・パッケージで利用する機能に対して、特別の調整が施されていたり、ネイティブ・コードで高速に実装されている可能性があります。現時点でパフォーマンス上の利得が望めなくとも、将来のリリースで調整されている可能性があります。また、独自実装のコードを除くことによってコードが簡潔になり、信頼性、標準化の点でも好ましい習慣です。

よく挙がる例が配列のコピーです。自分で実装する場合は、繰り返し内部で、要素を一つずつコピーすることになるでしょう。次の例は、配列src[]の4番目の要素から最後の要素までを、配列dest[]にコピーしています。

// src[]とdest[]は定義済みの配列
for (int i = 0; i < src.length; i++) {
	dest[i] = src[i + 5];
}

このように、単純にコピーするだけであれば、System.arraycopy()を使うことで、繰り返し内部で配列を使うことを避けることができます。次のコードは、配列src[]の2番目の要素から4つの要素を、dest[]の3番目の要素から後ろにコピーしています。

String[] src = {"0", "1", "2", "3", "4", "5" };
String[] dest = {"a", "b", "c", "d", "e", "f" };
System.arraycopy(src, 1, dest, 2, 4);

自分でコードを書くよりも、コア・パッケージで提供している機能を組み合わせて実現できないか探しましょう。

バッファの利用

オブジェクトにバッファを備えることで、パフォーマンスが上がることがあります。

コアパッケージでバッファを利用する代表的なものが、I/O処理のクラス群です。一般に、ディスク上やネットワーク越しのデータ・アクセスは、CPUの処理に比べて一万倍程度遅いとされます。CPUが高速化している今は、さらに10の数乗倍のオーダーで格差が発生しているはずです。更に、これらのストリームを利用する場合は、コネクションの確立をはじめとして、一般に処理が重くなります。このようなとき、メモリに余裕があれば、バッファの仕組みを検討する価値があります。

パッケージjava.ioのクラスには、バッファとオーバーフロー時の処理を自動化した仕組みを備えたクラスが用意されており、一定サイズのデータをガバガバとやり取りできます。例えば、クラスReaderを継承したFileReaderは、ファイルからのテキスト入力を、read(), readLine()で読み込みますが、16ビットのchar型配列を、一要素ずつ読み込むため、呼び出しごとにファイルからバイトを読み込み、文字型に変換し、そのたびに復帰するという重たい処理となります。これに対して、テキストを一定サイズ単位で読み込んで、効率を上げるストリームがBufferedReaderです。

BufferedReaderの場合、コンストラクタはReader型オブジェクトを引数にとるので、Readerクラスのサブクラス型の任意のストリームを引数に持つことができます。次のコードは、ファイル"inputFile.txt"からテキストを読み込むストリームFileReaderをBufferedReaderでラップしています。このストリームinによる読み込みでは、バッファを使った一定サイズの連続読み込みがサポートされます。

Reader in = new BufferedReader(new FileReader("inputFile.txt"));

バイト単位の入出力をおこなうStreamを使う場合も同様です。次のコードは、FileInputStreamをBufferedInputStreamでラップしています。

InputStream in = new BufferedInputStream(new FileInputStream("inputFile.dat"));

また、オブジェクトの生成時に、明示的にバッファのサイズを指定したり、カスタム・バッファを使うことで、よりパフォーマンスを高めることができます。

先述した「オブジェクトの生成の抑制」の項で、オブジェクトの生成を削減するために、生成したオブジェクトをプールするように勧めましたが、バッファを備えたクラス型オブジェクトは、再利用に不適切な場合があります。当該オブジェクトが、使われていくうちに成長していく場合は、再利用を繰り返して長期間にわたってメモリ上に存在し続けることで、実行環境のメモリ領域を圧迫する可能性があるのです。

例えば、先に挙げたStringBufferはそのタイプです。StringBuffer型オブジェクトは、バッファ内で変更可能な可変オブジェクトです。バッファが足りなくなれば、自動的にサイズが拡張します。他にも、コレクション・フレームワークのオブジェクトなどのように、自動的にサイズを拡張するクラスは多く存在します。多くの場合、バッファがオーバーフローすると、自動的に新規のオブジェクトを生成してサイズ拡張しますが、一旦確保した領域の解放は、自動的には実施しません。

StringBuffer型オブジェクトを使いまわす場合は、メソッドStringBuffer#capacity()でバッファサイズを確認したり、メソッドStringBuffer#ensureCapacity()で、成長可能なバッファの上限を定めるような工夫が必要です。一般に、バッファを持つほかのオブジェクトの場合も同様です。

重たい処理を細切れに実行する場合は、バッファの実装を検討しましょう。バッファを持つオブジェクトは、バッファの成長に対する歯止めを施しましょう。

IO クラス についての詳細は、本稿のコアパッケージの説明を参照くださ。

オブジェクトの明示的な廃棄

Javaでは、記憶域管理にGCを使います。GCを実装するアプリケーション言語としない言語とでは、メモリ管理の思想が異なります。GCを実装しない、ポインタを備えた言語の場合は、メモリ・リークをなくすと言う観点で、メモリ管理を実装します。一方、GC備えたオブジェクト指向のJavaの場合は、メモリ・リークと言う概念は存在せず、メモリ管理はインスタンス管理という概念で置き換えられます。

インスタンス管理で重要なのは、メモリ領域の明示的な制御ではなく、GCが適切に回収/再利用可能にできるように、参照を削除することです。例えば、パッケージjava.ioに含まれるクラスの場合は、明示的な参照の終了のために、メソッドclose()を提供しています。そのほかのクラス型参照の場合も、利用し終わって、使うつもりのないオブジェクト参照を保持する変数には、明示的にnullを代入することが有効です。

そのためには、当該オブジェクトのライフサイクルを明確にする必要があります。ローカル変数に格納された参照の場合は、当該変数が宣言されたブロックから制御が抜けると共に、スタック上からドロップされるために、ヒープ上の当該インスタンスへの参照が失われるので、対応するヒープ上のインスタンスがGCの回収対象になります。一方、フィールド(メンバー変数)に格納される場合は、当該オブジェクトが生存する限り、当該フィールドも生存するため、当該オブジェクトが回収対象にならない限り、参照するオブジェクトも回収対象にはなりません。

フィールドに宣言された変数に格納される参照のように、スコープが広範囲にわたる場合は、利用し終わった時点で、明示的にnullを代入して参照を消すことが有効です。これは、メモリ使用量の削減、パフォーマンスの向上の観点から有効です。設計上の観点でも、当該オブジェクトに責任を持つクラスの設計者が、外部のコードによって、想定しない利用をされることを避けるためにも有効となります。

close()などのメソッドが提供されている場合は、必ず呼び出すようにしましょう。自分が作ったクラスの場合も、close()に類する終了のメソッドを提供するようにしましょう。廃棄のメソッドが提供されていないオブジェクトを生成する場合も、設計段階で、オブジェクトの生存するスコープを明確化して、使い終わった参照にはnullを代入するようにしましょう。

GC (Garbage Collector) についての詳細は、本稿のパフォーマンス・チューニングの説明を参照くださ。

ロジック分岐のための例外処理

Javaでのロジック分岐の制御構造には、if-else if-else, switch 構造が用意されています。それに加えて、クラス java.lang.Exception を継承した例外をキャッチする try catch finally 構造も条件分岐の一つだといえます。

たしかに、try catch finally構造で制御を分岐することは可能ですが、例外発生を念頭にいれたロジックはパフォーマンスを悪化させます。ありがちなケースとして、繰り返しからの脱出のために、意図的に例外を発生させているケースがあります。繰り返し終了条件は、言語仕様として、for, while, do-while構造のなかで実装されているものです。例外の存在意義を正しく使っていないために設計として不適切である上に、パフォーマンス上の負荷が発生します。

例外発生時には、スタックのトレースが生成されます。例外が発生したメソッドから、メソッドの参照経路を遡っていった記録が、スタック・トレースです。メソッド java.lang.Throwable#printStackTrace() などによって取得可能で、障害をトレースするために必要な、例外発生箇所を特定するのに役立ちます。しかし、取得するかどうかに関わらず、例外発生時には常に生成されるものであり、パフォーマンス上は結構曲者です。

通常、スタックには膨大なメソッド呼び出しが積まれているものなので、スタック・トレースは膨大な行数になります。期待するコンピュータ制御のフローを制御するために例外を発生させる場合は、制御が分岐するたびに膨大なトレース情報を生成していることになります。制御分岐のために例外を発生させることは、明らかにコスト高です。繰り返し終了の条件に使う場合は、繰り返されるたびに、判定条件を実行しないで済む分、パフォーマンス上の利得がありそうですが、例外発生のコストのほうが高くなるでしょう。

一方、例外発生を検知する仕組み適切な設計であっても、スタック・トレースは必要ない場合があります。このような場合は、メンバーで空のException型オブジェクトを生成しておいて、これをthrowすることもできます。もちろん、このときException型オブジェクトが保持しているスタック・トレースは生成時点のものです。実行時のスタックの情報を含めたい場合は、java.lang.Throwable#fillInStackTrace()Throwable#setStackTrace() で明示的にセットする必要があります。

例外情報を取得する必要がないのであれば、例外を分岐構造の実現のために発生させるのは止めましょう。必要があれば、例外をthrowするのではなく、プリミティブ型の戻り値で制御しましょう。

例外処理 についての詳細は、本稿のJava のオブジェクト指向の説明を参照くださ。

メソッドのインライン化

メソッド呼び出しは、ヒープ上の呼び出し元のインスタンスから、スタックに積み上げられた参照を経由して、ヒープ上に展開されたメソッドへと辿ります。メソッド呼び出し元と実装の間には、内部的には複数個のポインタを経由する必要があります。一方、一部のメソッドでは、ヒープ上の呼び出し箇所に、スタックへのポインタの代わりに、実行されるメソッドの実装を展開することが可能となります。これを、インライン化と呼びます。オーバーヘッドは掛かりますが、多段組のポインタを経由するよりも、高速にコンピュータ制御が実行対象コードに到達できます。

インライン化を促進する仕組みの一つがアクセス修飾子のprivateと、クラス・メンバーを宣言するstaticです。private修飾子は、当該クラス内からしか参照できません。従って、private修飾されたインスタンス・メソッドへは、他の参照を気にせず、インライン化が可能なのです。また、static修飾子は、インスタンスに依存しないクラス型のメンバーを宣言します。従って、static修飾されたメソッドも、他への参照を気にせず、インライン化が可能となります。

一方、private修飾子は、自動的にfinalを意味します。final修飾子は、修飾対象によって、意味が異なります。

JDK 1.1相当のJVMに関する記述で、「final修飾されたメソッドはインライン化されるために高速である」との記述を見ることがあります。しかし、少なくともSDK 1.2相当以上のJVMでは、final修飾することによるパフォーマンス上の利得はあまり確認できません。パフォーマンスを目的としてfinal修飾することは、継承というオブジェクト指向設計の柔軟性を無効化する仕組みであって、相当の理由が無いのであれば慎むべきです。

修飾子privateは、オブジェクト指向設計でカプセル化と言う観点の下で、積極的に推奨されます。極限的なケースでは、全てのメソッドはprivateであるべきです。その中から、なにを外部のクラスに対して公開すべきであるのかを考えて、当該クラスの外部に対するインタフェースとなるメソッドを抽出します。必要に応じて外部に公開するという方針を採ることで、設計段階で不要なロジックを排除することにも繋がります。

修飾子staticは、インスタンスに依存するメンバーではなく、クラスに固有の共通機能を提供するために使われます。フィールド変数にオブジェクトを保持するような場合に、インスタンスごとに分けて持つ必要が無ければ、static修飾することで、生成されるオブジェクトの個数を減らすことができます。メソッドの場合も、インスタンス毎にコピーされることが無いために、ポインタとメモリの節約につながります。

パフォーマンス目的でのfinal修飾子が、設計に悪影響を及ぼす場合は、使うのは止めましょう。static, ptivate修飾子は、積極的に利用して、最低限度な物だけ、インスタンスのインタフェース・メソッドとして公開しましょう。

修飾子 についての詳細は、本稿のJava のオブジェクト指向の説明を参照くださ。



Copyright © 2003 SUGAI, Manabu. All Rights Reserved.