パフォーマンスへの目: 開発プロセスを改善する

コンパイル・スピード、例外、ヒープサイズがBig Moose Saloon常連の話題に

Comments

これまでの一月、我々はJavaRanchのBig Moose Saloonにじっと座り、JavaRanch牧場の新米たちが一体どういうパフォーマンスを要求しているのかを見てみました。大部分はJ2SEと開発手順について、つまりJava言語に関する質問、コア・クラスや開発過程をどう改善すべきか、というようなものでした。

コンパイル・スピード

コンパイル・フェーズが遅いのが分かりましたか? javacに時間がかかりすぎますか? では.class生成にあの例の、「すっ飛ばすやつ」Jikesコンパイラを試してみてください。このJikesは完璧にJavaソースをサポートする、全く新しい、超新鮮Jikesなのです。(VerifyErrorを起こすかもしれません。全部のjavacオプションをサポートしているわけではありません。バイトコードは宣伝されているほどではないかもしれません。それにパフォーマンスは一定ではありません。使用前には必ずマニュアルを読んでください。)

よろしい。つまりJavaRanchでのJikesについての議論は、ここ、我らの手作り広告ほどには直接ではありませんでしたが、一部の読者ははっきりと、Jikes Javaコンパイラは高速コンパイルのために作られていると言っていました。これは、特にコンパイルするファイルが大量にあるときには、知っていると役に立つ情報です。ただし、Jikesが開発プロセスを高速化するのは確かですが、最後のコンパイルは製品に使用予定のJVMに付いてくるコンパイラを使った方が無難です。色々な要素がJVMのバージョンによって大幅に違うので、JVMのものとは別のコンパイラを使うと問題が起きる可能性があるからです。

例外は高くつく

そう、例外は高くつきます。だから絶対例外を使うべきではない、ということですか? もちろんです。では一体どんなときには例外を使うべきで、どんなときには使うべきでないのでしょうか。残念ながら、答えは簡単ではないのです。

今まで教わってきた、良きtry-catch式プログラム習慣を捨て去る必要は無いと言えるのですが、このやり方では問題に突き当たってしまう場合があります。例外を生成するときです。例外が生成されるときには、どこでその例外が生成されたかを記述するスタック・トレースを集めてくる必要があります。予期しない例外がコードに投げ込まれたときにプリントアウトされる、そういうスタック・トレースを覚えていますか。こんな風な感じです。

Exception in thread "main" my.corp.DidntExpectThisException
        at T.noExceptionsHere(T.java:13)
        at T.main(T.java:7)

こうしたスタック・トレースを構築するにはランタイム・スタックのスナップショットをとる必要があり、それが高くつくのです。ランタイム・スタックは効率よく例外を生成するように作られてはおらず、ランタイムをできるだけ早く走らせるように作られているのです。無用なディレイなしに用をすませるため、とにかくプッシュ・ポップ、プッシュ・ポップ。ところがExceptionを生成するとなると、JVMはこう言わなければならない。「止まれ! 写真を撮るからプッシュ・ポップをちょっと待ってニッコリ笑って!」 スタック・トレースにあるのはスタックの要素一つか二つだけではありません。頭からお尻まで、行番号からなにから、あらゆる要素が入っています。もし深さ20のスタックに例外が生成されるとすると、先頭の数要素だけを記録しておけば良し、というようなオプションはあり得ません。20全部を受け取ることになるのです。このスタックはmainもしくはThread.run(スタックの一番下)からずっと先頭までを記録しているのです。

ですから例外の生成は高くつくのです。技術的にはスタック・トレースのスナップショットはネイティブ・メソッドThrowable.fillInStackTrace()で起こり、このメソッドはThrowableコントラクタで呼ばれます。ただ、そう言ったから何かが変わるわけではありません。Exceptionを生成すれば、その代償を払う羽目になるのです。幸い、例外を捕捉する代償はそう高いものではありません。ですから心行くまでtry-catch手法を使えばいいわけです。また下に示すように、パフォーマンスを低下させること無く、メソッド定義の中でthrows文を定義することも出ます。

public Blah myMethod(Foo x) throws SomeBarException {
  ....

技術的には、代償など無く適当に例外を投げることもできます。パフォーマンス低下を招くのはthrowオペレーションではなく(まず例外を生成せずに例外を投げるのは普通ではないのですが)、例外を生成することがパフォーマンス低下を招くのです。

try {
  doThings();
  if (true)
    throw new SomeException(); //cos my program runs too fast
} 
catch(SomeException e) {
  doMoreThings();
}

幸い、良いプログラミング習慣を身につけるには、適当に例外を投げるものではないと教わってきているはずです。例外は例外的な条件のために考えられたものであり、その基本を崩すべきではありません。ただ、何かの理由で良いプログラミング習慣に沿いたくない場合にJava言語には、プログラムをより早く走らせたいなら走らせることもできる手段も用意されていると思ってください。

最大ヒープ・サイズ

我々が訪れたディスカッション・グループではどこでも必ず、JVMのヒープをどう調整するかという議論が繰り返し飛び出していました。あるJavaRanchでは基本的な質問「最大ヒープ・サイズをいくつに設定すべきか」で議論が始まっていました。詳細に入り込む前に、Javaランタイムでのメモリ管理の基本を見てみましょう。

JVMはJVMが管理するメモリ空間を持っています。その空間の一部、オブジェクトが生まれる(そして死ぬ)空間をヒープ・スペース と言います。オブジェクトはヒープ・スペースで生成され、JVMガーベジ・コレクタによって何度か、例えばヒープのデフラグメント(またはコンパクティング )時にヒープ・スペースを引き回されます。オブジェクトはまた、ヒープで死にもします。死んだオブジェクトとは単に、アプリケーションからもはやアクセスできなくなったオブジェクトを言います。JVMのガーベジ・コレクタはこうした、死んだオブジェクトを探し、そうしたオブジェクトが使っていた空間を解放し、新しいオブジェクトが使えるようにします。ガーベジ・コレクタが、死んだオブジェクトの使っていた空間を解放しきってしまい、もはや解放できる空間が無くなると、ヒープが一杯 だと言うのです。

ヒープが一杯と言うのは問題です。ヒープが一杯の時にアプリケーションがもっとオブジェクトを生成しようとすると、JVMがオペレーティング・システムに対してもっとメモリを要求し、ヒープをより大きくしてしまう可能性があるのです。JVMがもしメモリを確保できないと、新しいオブジェクト割り当てようとOutOfMemoryErrorが投げられます。これは、アプリケーションがよほど高度な機能を持っていない限り、アプリケーションがクラッシュすることを意味します。

ではどうしたら良いのでしょう。大部分のJVMにはオプションのパラメータがあり、ヒープが大きくなれる最大サイズを規定できるようになっています。そのサイズに達すると、JVMはオペレーティング・システムに対してメモリを要求できなくなるのです。SunとIBMが提供する最近のJVMでは、そのパラメータは-Xmxオプションで規定されます。これらよりも前のJVMでは-mxパラメータを使っていて、大部分のJVMは今でもこのオプションを理解します。アプリケーション・サーバーには最大ヒープ・サイズを規定する、それぞれ独自のコンフィグレーション・パラメータがあり、これらパラメータは通常-Xmxパラメータに渡されます。-Xmxパラメータを使うかはっきりしない場合に備えてJVMはデフォルトのヒープ・サイズを持っていますが、そのサイズは当然ベンダーやバージョンによって異なることになります。The Sun 1.4 JVMでの最大ヒープ・サイズのデフォルト値は64メガ・バイトです。

では最適パフォーマンスを保証する最大ヒープ・サイズはいくつにすべきなのでしょう。メモリ不足エラーが出ないように、またアプリケーションができるだけ多くのメモリを使えるように、「できるだけ大きく」が答えだと思われるかもしれません。実はオペレーティング・システムがどう動くかを考えれば、ヒープが大きすぎると重大な問題になり得るのです。具体的に言うと、最近のオペレーティング・システムではリアル ・メモリとバーチャル ・メモリを使います。バーチャル・メモリはリアル・メモリをスワップ・ファイル(オーバーフロー・メモリのように働きます)のディスク・スペースで補うことで、実際のメモリよりも大きなメモリがあるような幻想を抱かせるものです。オペレーティング・システムはアクティブに使われていないページを、再度必要になるまでディスクに置きます。こうすることでリアル・メモリを(一時的に)他の用途に使えるようにするのです。このため、使えるメモリのサイズは実際のメモリ・サイズよりも大きく見えるようになり、大きなプロセスを走らせられるようになります。その代償として、ディスクに置かれたページは必要な時にリアル・メモリに戻される必要があるのですが、これが非常に遅くなってしまうのです。ディスクはメモリよりもはるかに遅いですから。

もしヒープをシステムのリアル・メモリ(そのマシンに搭載されている物理的なRAMサイズ)よりも大きくしてしまうと、ヒープはページングを始めてしまいます。それ自体はたいした問題ではないかもしれません。結局それほど頻繁には使われないページがディスクに置かれるだけですから。ところがガーベジ・コレクションとなるとヒープの全体がスキャンされるので、そうした、めったに使われないページもリアル・メモリに展開する必要が出てきます。つまりその古いページのスペースを作るために、リアル・メモリ上にある、他のページをディスクに移動しなければならないのです。これが悪しき循環となってしまうのです。なぜなら今ディスクに移動したページは(ディスク上にあると言うことで)すなわちヒープ上でめったに使われないページと言うことになり、そのヒープこそ、まさにガーベジ・コレクタがスキャンしようとしているヒープ、となるからです。結果として、ページをメモリに出し入れするのに時間が大幅にかかってしまい、「まともな」仕事ができない、と言うことが起きてしまいます。

ガーベジ・コレクションは、それでなくてもアプリケーションのボトルネックになりがちなものです。ヒープを余りにも大きくしてしまうと、JVMがガーベジコレクションするためにオペレーティング・システムは大規模なページングをせねばならず、それが非常に遅いページングを引き起こし、結果としてアプリケーションが「這うように」遅くなることになります。ですから、このページングの悲劇が起きないように、また同時に実行される他のプロセスも考慮に入れながら、最大ヒープ・サイズは実際のシステムRAMよりも確実に小さく なるように設定しなければなりません。


ダウンロード可能なリソース


関連トピック

  • JavaRanch のBig Moose Saloonでパフォーマンスに関する疑問を他の参加者と共有してみてください。
  • ヒントや参考資料が豊富にあるJava Performance Tuning Webサイトを見てみてください。
  • Jack Shirazi著のJava Performance Tuning (O'Reilly & Associates, 2003年1月刊)には、この記事にあるようなヒントがもっとたくさん載っています。
  • 無料のJikes compilerコンパイラをダウンロードして、コンパイルをスピードアップしてみてください。
  • アプリケーションのパフォーマンス調整のケース・スタディには「Javaアプリのパフォーマンスを最大限引き出す実践ガイド」(developerWorks 2002年6月)を見てみてください。
  • developerWorks Java technology zoneにはJavaプログラミングのあらゆる面について無数の記事があります。

コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology
ArticleID=218902
ArticleTitle=パフォーマンスへの目: 開発プロセスを改善する
publish-date=07302003