この章では、Solaris 8 環境において Java アプリケーションのパフォーマンスを向上させる方法について説明します。アプリケーションのパフォーマンスとは、そのアプリケーションの資源の使用量と定義することができます。パフォーマンスチューニングとは、資源の使用量を最低限に抑えることです。
ここで紹介するパフォーマンスチューニングに関するヒントの多くは、Solaris 2.6、Solaris 7、および Solaris 8 で使用する Java に固有です。将来のリリースでは、パフォーマンス特性が変わることが予想され、ここで紹介しているヒントが適切でなくなる可能性があります。
以降で説明するように、チューニングはいくつかのレベルで行うことができます。
チューニングによって大幅なパフォーマンスの向上が見込める Java システムのインタフェースには、次のものがあります。
入出力
文字列
配列
ベクトル
塗りつぶしと描画
ハッシュテーブル
イメージ
メモリー使用
スレッド
以下のコンパイラによる最適化が可能です。
Java コンパイラ
JIT コンパイラ
パフォーマンス向上のためには、以下の部分のコードをチューニングします。
ループ
真偽式をテーブルルックアップに変換する
キャッシュ
事前に計算を行う
評価の引き延ばし
クラス - オブジェクトの初期化
一般的に、Java アプリケーションで最も一般的で大きなパフォーマンス上の問題は、非効率的な入出力です。このため、一般的に入出力の問題は、Java アプリケーションのパフォーマンスチューニングで最初に検討すべき問題になります。入出力の問題を解決することによって、その他のすべての最適化を行なった場合よりも大幅にパフォーマンスが向上することがあります。効率的な入出力手法を用いることによって、10 倍以上の速度の向上が得られることも珍しくありません。
アプリケーションが大量の入出力を行う場合は、入出力のパフォーマンスチューニングを行なってみてください。チューニング結果は、アプリケーションをプロファイルすることによって確認できます。Java アプリケーションのプロファイルするには、Sun の Java WorkShopTM 製品を使用することができます。Java WorkShop は、以下の URL から入手することができます。
Java WorkShop のオンラインヘルプで、プロファイラまたはプロファイルについての説明を参照してください。以下の例は、4 つの異なるメソッドを使用して、150,000 行からなるファイルを読み取るベンチマークテストの結果です。
DataInputStream.readLine() のみ (バッファーなし)
DataInputStream.readLine() と BufferedInputStream (2048 バイトのバッファーあり)
BufferedReader.readline() (8192 バイトのバッファーあり)
BufferedFileReader(fileName)
結果は次のとおりです (単位: 秒)。
DataInputStream: | 178.740 |
DataInputStream(BufferedInputStream): | 21.559 |
BufferedReader | 11.150 |
BufferedFileReader | 6.991 |
メソッド 1 と 2 では Unicode の文字が正しく処理されませんが、メソッド 3 と 4 では Unicode 文字が正しく処理されることに注意してください。つまりほとんどの製品では、メソッド 1 と 2 は使用できないことになります。JDK 1.1 では、DataInputStream.readLine() も推奨されません。Java WorkShop とその他のプログラムでは、メソッド 1 が使用されています。
Solaris の入出力処理の問題を見つけるためのもう 1 つの方法として、truss(1) を使用して、read(1) と write(1) システムコールを検索するという方法もあります。
文字列に関して忘れてならない最重要事項は、ループで文字を処理するときには、String や StringBuffer クラスではなく必ず char 配列を使用することです。配列要素へアクセスする方が、charAt() メソッドを使用して文字列内の文字にアクセスするよりもはるかに高速です。また、文字列定数 ("...") はすでに文字列オブジェクトであることを忘れないでください。
//DON'T
String s = new String("hello");
//DO
String s = "hello";
String クラス
ループ内の可変文字列や文字処理、charAt() メソッドで String クラスを使用しないでください。
StringBuffer クラス
StringBuffer クラスは、文字列が可変で、複数のスレッドによって並列にアクセスされ、文字処理が行われない場合にのみ使用してください。ループ内の非可変の文字列や文字処理、charAt() メソッド、setCharAt() メソッドには使用しないでください。デフォルトの文字列サイズは 16 文字です。このクラスは、文字列を連結するときにコンパイラによって自動的に使用されます。最大の文字列サイズがわかっている場合は、初期バッファーサイズとしてそのサイズを設定してください。
StringTokenizer クラス
StringTokenizer クラスは、簡単な解析や読み取り走査に役立ちますが、非常に非効率的です。このクラスは、String ではなく文字配列に文字列や区切り文字を格納するか、最上位の区切り文字を格納して、より短時間に検査が行われるようにすることによって最適化できます。区切り文字リストや処理文字列によって異なりますが、このような最適化によって、1.6 倍から 10 倍 (通常は 2.4 倍程度) にパフォーマンスが向上します。
配列は境界が検査されるので、その分パフォーマンスが低下します。ただし、配列へのアクセスは、ベクトルや String、StringBuffer にアクセスするよりははるかに高速です。より高いパフォーマンスを得るには、System.arraycopy() を使用してください。これはネイティブメソッドであり、手動の配列処理よりもかなり高速です。
Vector は便利ですが非効率的です。最高のパフォーマンスを得るには、構造体のサイズが不明で効率性がそれほど重要でない場合にのみ使用してください。Vector を使用する場合は、パフォーマンスが低下するのでループ内で elementAt() を使用しないでください。Vector は、次の特徴を持つ配列に対してのみ使用してください。
複数のスレッドによって同時にアクセスされる
サイズが動的に変化する
HashTable には、以下のチューニング可能なパラメータがあります。
initialCapacity (容量、通常は素数): 十分な大きさに設定されていないと、衝突が発生し、ハッシュ処理が停止して、その後線形リスト処理が実行されます。
loadFactor (負荷率、0.0 から 0.1 の範囲): 容量を超えてテーブルが拡張される割合です。HashTable は hashCode() を呼び出します。これらのクラスには、あらかじめ定義されている hashCode() メソッドがあります。
Color、Font、Point
File
Boolean、Byte、Character、 Double、 Float、 Integer、 Long、 Short、 String
URL
BitSet、Date、 GregorianCalendar、 Locale、SimpleTimeZone
長さによっては、String.hasCode() が必ずしもすべての文字をサンプリングするわけではないことに注意してください。
1〜15 文字の長さ: すべての文字
16〜23 文字の長さ: 1 文字おき
24〜31 文字の長さ: 2 文字おき
イメージについては、次のような方法があります。
塗りつぶしと描画のパフォーマンスを向上させるには、次の方法を使用してください。
ダブルバッファリング (たとえば、アニメーションではオフスクリーンにイメージを描画して全体を一度に読み込みます)
update() 関数によるデフォルト値以外の使用 (オーバーライド)
public void update(Graphics g) { paint(g); } |
カスタマイズした独自のレイアウトマネージャ の使用。独自の動作が必要な場合は、そのためのコードを作成することによって、最高の GUI パフォーマンスを得ることができます。
イベントの使用。JDK 1.1 には、1.0 に比べて効率的なイベントモデルが用意されています。
損傷を受けた部分だけの再描画 (ClipRect を使用)。
非同期の読み込みパフォーマンスを向上させるには、独自の imageUpdate() メソッドを使用して imageUpdate() をオーバーライドします。imageUpdate() は、必要以上に再描画を行うことがあります。
//wait for the width information to be loaded while (image.getWidth(null) == -1 { try { Thread.sleep(200); } catch(InterruptedException e) { } } if (!haveWidth) { synchronized (im) { if (im.getWidth(this) == -1) { try { im.wait(); } catch (InterruptedException) { } } } //If we got this far, the width is loaded, we will never go thru // all that checking again. haveWidth = true; } ... public boolean imageUpdate(Image img, int flags, int x, int y, int width, int height) { boolean moreUpdatesNeeded = true; if ((flags&ImageObserver.WIDTH)!= 0 { synchronized (img) { img.notifyAll(); moreUpdatesNeeded = false; } } return moreUpdatesNeeded; } |
イメージのデコードは、読み込みより長い時間がかかります。PixelGrabber と MemoryImageSource を使用して事前にデコードすることによって、複数のイメージを 1 つのファイルにまとめ、最高の速度が得ることができます。この方法は、ポーリングを行うよりも効率的です。
アプリケーションのパフォーマンスは、実行中のガーベッジコレクション量を少なくすることによって大幅に向上させることができます。また、次の方法によってもパフォーマンスを向上できます。
次のコマンド使用して、初期ヒープサイズをデフォルトの1M バイトより大きくする。
java -ms number. java -mx number.
デフォルトのヒープサイズは最大で 16M バイトです。
次のコマンドを使用して、メモリーを多く使いすぎる部分を見つける。
java -verbosegc
配列を割り当てるときにサイズを考慮する (たとえば short で十分ならば、int の代わりに short を使用する)。
ループ内でのオブジェクトの割り当て (readLine() など) を避ける。
「Solaris 環境における従来の Java スレッド *」で説明したように、アリケーションのパフォーマンスは、ネイティブメソッドを使用することによって大幅に向上します。グリーンスレッドがタイムスライスされることはないため、実行状態を示すには、ループ内での Thread.yield() の呼び出しが必要になり、実行速度が低します。その他、次の方法は使用しないでください。
同期の過度の使用。コーディングエラーが原因のデッドロックや、ロック競合が原因の遅延が発生する可能性が大きくなります。また、頻繁な同期によって得られる利点よりも、オーバーヘッドの方が大きくなる可能性もあります。このような場合には同期を最小限にした方がパフォーマンスが向上します。
ポーリング。外部のイベントを待つときに、副次的なスレッド (メインスレッド以外のスレッド) 中で実行される場合にのみ、ポーリングを行うことができます。ポーリングの代わりに wait() および notify() を使用してください。
Java コンパイラと JIT コンパイラは、次のような最適化を自動的に行います。
インライン化
一定の折り返し処理
配列境界検査を行わない
ブロック内で共通の部分式を省略
空のメソッドを省略
ローカルのレジスタ割り当ての一部を省略
フロー分析を行わない
インライン化を限定する
パフォーマンスを向上させるには、次のことを守ってください。
ループ不定正規は、ループの外に移動します。
テストは、できるかぎり単純にします。
ループ内では、ローカル変数だけ使用します。ループに入る前にローカル変数にクラスフィールドを割り当ててください。
値が定数の条件式はループの外に移動します。
同様のループは結合します。
ループが交換可能な場合は、最も頻繁に実行されるループを入れ子にします。
最後の手段として、ループを展開します。
値がある範囲の小さな整数である 1 つの式に基づいて値が選択される場合は、テーブルルックアップに変換してください。条件分岐があると、コンパイラによる最適化の多くが行われなくなります。
キャッシュによってメモリーの使用量は増えますが、パフォーマンス向上に利用することができます。フェッチや計算に重い負荷がかかる値は、キャッシュを利用してください。
コンパイル時にわかっている値を事前に計算しておくと、パフォーマンスが向上します。
必要になるまで結果の計算を遅らせると、起動時間が短縮されます。
1 回だけ行われる初期化をすべて 1 つのクラスイニシャライザでまとめて行うようにすると、パフォーマンスが向上します。