BT

最新技術を追い求めるデベロッパのための情報コミュニティ

寄稿

Topics

地域を選ぶ

InfoQ ホームページ アーティクル Javaのパフォーマンスについての9つの誤信

Javaのパフォーマンスについての9つの誤信

原文(投稿日:2013/04/23)へのリンク

Javaのパフォーマンスはダークアートのようなものだ,という評価があります。 プラットフォームが技術的に高度で,多くの状況において原因の特定が難しくなっていることがその理由のひとつです。その一方でJavaのパフォーマンステクニックには,歴史を重ねたひとつのトレンドが存在しています。ただしそれは統計的手法や実証的議論の結果というより,むしろ民間伝承の寄せ集めのようなものです。今回の記事ではこのような,言わば技術的神話の中で,もっとも悪質なものを解決したいと思います。

1.Java は遅い

Javaのパフォーマンスに関する古くからの誤信の中でも,おそらくこれがもっとも強く信じられているものでしょう。

確かに90年代から2000年代初頭に戻って考えれば,遅かった時代もあるかも知れません。

しかしながら,10年以上に及ぶ仮想マシンの改良とJIT技術の功績によって,現在ではJavaの総合性能は驚くほど向上しています。

6項目で構成される Webパフォーマンスベンチマーク を見ても,各項目のトップ4に当たる24のうち22をJavaで記述されたフレームワークが占めている程です。

JVMはプロファイリングを利用してコードの最適化を行います。対象は頻繁に利用されるコードパスのみですが,徹底的に行うことで大きな効果を上げています。JITコンパイルされたコードに関しては,現在では多くの場面において (その割合も増えつつあります) C++の実行速度を凌駕しています。

このような事実にも関わらずJavaが今でも低速なプラットフォームとして認識されているのは,おそらくは初期バージョンのJavaプラットフォームでの経験が,歴史的な負のバイアスとして働いているためでしょう。

早まった結論を出す前に,客観的な見地に立って,最新のパフォーマンス結果を評価するようにお勧めします。

2. Java コードの1行にはそれ自体で意味がある

次の短いコード行を考えてみてください:

MyObject obj = new MyObject();

Java開発者ならば誰でも分かるように,このコードはオブジェクトを割り当てて,適切なコンストラクタを実行するものです。

ではここで,パフォーマンス上の境界について検証をしてみましょう。私たちはこの行で一定の処理が実行されることを知っていますから,その推測に基づいて,パフォーマンス上の影響が算出可能だと信じています。

ここにある種の認知バイアス(cognitive bias)があります。私たちは,すべての処理が実行される必要がある,という先入観にとらわれているのです。

実際にはjavacやJITコンパイラが,デッドコードを最適化して削除するかも知れません。 JIT コンパイラに関して言えば,プロファイル情報に基づいて推測的なコードの最適化削除が行われる可能性もあります。そうなればコードはまったく実行されませんから,パフォーマンスへの影響もゼロになります。

さらにJRockitなどいくつかのJVMでは,JIT コンパイラがオブジェクト操作を分解することができます。コードパスが完全にデッドコードではない場合でさえ,アロケーションが実行されない可能性があるのです。

ここでの教訓としては,Javaのパフォーマンスを扱う場合にはコンテキストが重要であること,早まった最適化 (premature optimization) が直感に反する結果をもたらす可能性があること,この2つです。最良の結果を得るには,最適化に過度に頼ってはいけません。その代わりに,パフォーマンスのホットスポットを見つけ出して修正する,というパフォーマンスチューニングテクニックを用いて,常にコードを構築することが大切なのです。

3.マイクロベンチマークにはその結果通りの意味がある

これまでに見たように,コードのごく一部を対象とした検証は,アプリケーション全体のパフォーマンス解析に比べると正確さの面で劣ります。

それにも関わらず,開発者はマイクロベンチマークを書くのが大好きです。プラットフォームの低レベルな面をいろいろ試す作業から得る喜びは,ある種の人々にとっては無限大であるようです。

Richard Feynman氏がかつて言ったように "一番のルールは自分自身を欺かないことだ。 そして一番欺きやすい人間はあなたである。" Javaのベンチマークを書くとき以上に,この言葉が当てはまる場所はありません。

優れたマイクロベンチマークを書くというのは,本当に難しい作業です。Javaプラットフォームは高度で複雑なため,マイクロベンチマークが測定に成功しているのは,実は一時的な影響であったり,あるいは意図しないプラットフォームの側面であったり,ということもあります。

例えば,単純に書かれたマイクロベンチマークでは,それが捕捉しようとした影響ではなく,実行管理サブシステムや,おそらくはガベージコレクションを測定していた,ということも少なくありません。

本当に必要な開発者あるいはチームだけが,マイクロベンチマークを書くべきです。作成したベンチマークは,全体を(ソースコードを含めて) 公開して再現性を確保するとともに,十分な査読と精査を受ける必要があります。

Java プラットフォームには多数の最適化機能があるため,個別のベンチマークの実行結果の統計には問題があります。本当に信頼できる結果を得るには,単一のベンチマークを何度も実行して,その結果を集計することが必要なのです。

本当にマイクロベンチマークを書かなければならない状況ならば,Georges, Buytaet, Eeckout 共著の論文 "Statistically Rigorous Java Performance Evaluation (統計的厳密性を持った Java パフォーマンス評価)" を読むことから始めるのがよいでしょう。統計を適切に取り扱わなければ,すぐに判断を誤ってしまいます。

十分に開発されたツールやそれを取り巻くコミュニティがすでに存在します (Google の Caliper など) から,どうしてもベンチマークを書かなければならない場合でも,自分ひとりでは行わないでください – 仲間の視点や経験といったものが必要なのです。

4.アルゴリズムの遅さがパフォーマンス問題のもっとも多い原因である

開発者の間で (一般の人々の間でも) 非常に多い誤信のひとつに,自分たちがコントロールする部分がシステムの重要部分である,という考え方があります。

Javaのパフォーマンスの問題ではこれが,アルゴリズム上の品質がパフォーマンス問題の重要な原因である,というJava開発者の考え方として現れます。開発者が考えるのはコードですから,その思考がアルゴリズムの方向に偏るのはある意味で自然なことでしょう。

しかし実際には,現実世界のパフォーマンス問題全般への取り組みにおいて,アルゴリズム設計が根本的な問題であると判断されることは全体の10%もありません。

ガベージコレクションやデータベースアクセス,設定ミスなどが,アプリケーションを遅くする原因であることの方が多いのです。

大部分のアプリケーションは比較的小規模なデータを扱います。ですからアルゴリズムの非効率性がパフォーマンス上の重要な問題の原因となることはほとんどありません。開発をしている私たちは,採用したアルゴリズムが次善策であったことを知っています。しかし,もしそうだとしても,それによって加えられた非効率性は,他に比べればずっと小さなものです。アプリケーションスタックの他部分の,パフォーマンスへの影響の方がはるかに大きいのです。

ですから私たちにできる最高のアドバイスは,”パフォーマンス問題の真の原因を明らかにするには,経験に基づいた実データを使うべきだ”,ということになります。推測ではなく,計測するのです!

5.キャッシュがすべてを解決する

"コンピュータ科学におけるすべての問題は,新たな間接化 (Indirection) を加えることで解決できる。"

David Wheeler氏 (と,インターネットのおかげで分かった,少なくともあと2人のコンピュータ科学者) によるこのプログラマ格言は,特にWeb開発者の間には驚くほど広まっています。

既存のアーキテクチャが十分に理解できていない状況で,情報過多によって分析不能に陥ったような場合に,このような誤信がしばしば発生します。

威圧感を覚えるような既存システムを扱うよりも,その手前にキャッシュを配置して内部を隠蔽して問題が解決することを望む,という方法を選択する開発者はよくいます。当然ですが,このアプローチはアーキテクチャ全体を複雑にする上に,次の担当者が製品の状況を理解しようとする場合の状況を悪化させているだけです。

巨大で広大なアーキテクチャでも,一度に作成されるのは1行あるいは1つのサブシステムです。しかし多くの場合,リファクタされてシンプルになったアーキテクチャの方がパフォーマンスはよいものですし,ほとんどの場合は理解も容易です。

ですから,キャッシュが本当に必要なのかを評価するときには,基本的な使用状況の統計 (ミス率,ヒット率など) を収集して,キャッシュ層の本当のメリットを立証できるように考えてください。

6.アプリはすべて ”Stop-The-World” を考慮しなければならない

Java プラットフォームにおける不可避の現実として,すべてのアプリケーションスレッドは,ガベージコレクション実行のために定期的に停止しなければなりません。現実的な証拠のない場合にも,これが重大な問題点として強調されることがあるのです。

 

人は通常,200ミリ秒に1回以上の頻度で発生する数値データの変化 (価格変動のような) を知覚できないことが,研究で実証されています。

その結果として,おもに人が利用するアプリケーションの一般的な目安として,200ミリ秒以下の STW (Stop-The-World) による停止時間であれば,通常は考慮する必要はありません。特定のアプリケーション (ビデオストリーミングなど) ではもっと低い GC ジッタが必要ですが,ほとんどの GUI アプリケーションはそうではないでしょう。

 

200ミリ秒の停止が容認されないアプリケーションというのは,ごく限られています (低レイテンシのトレーディングや機械制御システムなど) 。この種のアプリケーションでない限り,ガベージコレクタの影響をユーザが認識することはまずないでしょう。

物理的なコア数より多いアプリケーションスレッドを持つシステムであれば,オペレーティングシステムのスケジューラが CPU のタイムスライスアクセスのために介入しなければならない,という点も考慮する必要があります。"Stop-The-World" は恐ろしく聞こえますが,実際はどんなアプリケーションでも (JVMであってもなくても) コンピュータリソースの競合には対処しなくてはならないのです。

計測を行わなければ,JVMのアプローチがアプリケーションのパフォーマンスに現実的に影響を及ぼしているかどうか,明らかにはなりません。

要約すると,停止時間が実際にアプリケーションに影響しているか,GC ログを有効にして確認する必要があります。次にログを解析 (手作業,あるいはスクリプトやツールを使用) して,停止時間を算出します。停止がアプリケーションドメインにおいて本当に問題なのか,判断してください。重要なのは,もっとも厳しい質問を自分自身にすることです – ユーザは実際に不満を持っているのか?

7.オブジェクトプーリングの自作は広範囲のアプリケーションに適している

Stop-The-World停止はなんとなく気持ち悪い,という感覚への一般的な対策のひとつは,アプリケーショングループ用に独自のメモリ管理技術をJava ヒープ内に開発する,というものです。多くの場合これは,オブジェクトプール (あるいはさらに,本格的な参照カウンタ) を実装するというアプローチに至って,そこで管理するドメインオブジェクトを使用するためのコードが必要になります。

このテクニックはほぼすべての場合,間違っています。この方法は遠い過去,まだオブジェクト生成処理が高価でオブジェクト変更処理が安価である,とされていた時代のものです。今では状況がまったく違います。

現在のハードウェアでは,メモリ確保は驚くほど効率的になりました。最新のデスクトップやサーバハードウェアならば,メモリの帯域幅は少なくとも2~3GBはあるはずです。これは非常に大きな値で,ごく専門的な用途を除けば,現実のアプリケーションがこれほどの帯域を飽和させることはまずありません。

一般的に言って,オブジェクトプーリングを正しく実装するのは困難です (特に複数のスレッドが動作する場合)。しかも一般的な使用においては,この選択を不適切と判断させるような,否定的な要件もいくつかあります。

  • そのコードを使う開発者はすべてプーリングを意識して,適切な処理を行わなければならない
  • "プール対応" と "プール非対応" のコードのバウンダリを意識するとともに,ドキュメント化されなければならない
  • 複雑な追加機能を常に最新状態に維持すると同時に,定期的な見直しも実施しなければならない
  • 何らかの問題が発生した場合,認識不可能なデータ破損が発生する (Cのポインタの再利用と同じように) ような処理が再導入されることになる

要するにオブジェクトプールの使用は,GCの停止が許容されず,チューニングやリファクタなどのインテリジェントな方法では許容可能なレベルにすることができない場合に限定すべきなのです。

8.CMS GCはParallel Oldよりも常によい選択である

Oracle JDKでは古い世代のインスタンスを回収するために,デフォルトではパラレルの Stop-the-world コレクタを使用します。

これとは別の選択肢がCMS (Concurrent-Mark-Sweep) です。CMSではGCサイクルのほとんどの期間,アプリケーションスレッドの実行継続が可能になりますが,パフォーマンスロスが大きいなど,注意すべき点も多数あります。

GCスレッドとアプリケーションスレッドの同時実行を可能にするということは,必然的にオブジェクトの有効性に影響のある方法で,アプリケーションスレッドのオブジェクトグラフを操作する結果となります。この処理には事後のクリーンアップが必要なので,CMSは事実上2つ (通常はとても短い時間ですが) のSTWフェーズを持つことになります。

この結果,いくつかのことが発生します。

  1. すべてのアプリケーションスレッドが安全なポイントに入らなければならないため,フルGCごとに2回の停止が発生する。
  2. コレクションが並行して動作している間,アプリケーションのスループットが低下 (通常50%まで) する。
  3. CMSによるガベージコレクション中にJVMが消費するブックキーピング (とCPUサイクル) の総量は,パラレルコレクションに比べて相当高くなる。

これらのコストに見合う価値があるかどうかは,アプリケーションの状況によって違ってきます。フリーランチのようなものはありません。CMSコレクタは素晴らしいエンジニアリングの産物ですが,万能薬ではないのです。

ですからCMSを自身のGCストラテジと決める前に,まずはParallel OldによるSTW停止が許容も調整も不可能なのかどうか,判断をすべきです。そして最後に (いくら強調しても足りませんが),すべての測定を実運用と同等のシステムで行うことを確認してください。

9. ヒープサイズの拡大でメモリ問題は解決する

アプリケーションに問題があり,その原因としてGCが疑われる場合,多くのアプリケーショングループは単にヒープサイズを増やして対処しようとします。ある種の状況下ではこれで問題が解決して,他の対処方法を検討する時間を省けるかも知れません。しかしながら,パフォーマンス問題の原因を完全に理解していない状況では,この方法が事態をさらに悪化させるかも知れないのです。

多過ぎるドメインオブジェクト (2~3秒程度の寿命のものが中心だとしておきます) を生成するような,出来の悪いコードを持ったアプリケーションがあるとしましょう。アロケーション率が十分に高ければ,ガベージコレクションがすぐに実行されて,ドメインオブジェクトはTenured(古い)世代に昇格します。昇格したドメインオブジェクトはすぐに死ぬのですが,次のフルGCが実行されるまで回収されません。

このようなアプリケーションでヒープサイズを拡大したとしても,比較的寿命の短いドメインオブジェクトが増殖して死んでいくためのスペースが増えるだけのことです。Stop-The-World 停止時間が悪化するだけで,アプリケーションには何のメリットもありません。

ヒープサイズの変更やその他のパラメータのチューニングをするには,事前にオブジェクトアロケーションと寿命に関するダイナミクスを理解しておくことが不可欠です。計測せずに行動すれば,事態を悪化させるだけです。ガベージコレクタの出力する Tenured 領域の分散情報は,ここでは特に重要なものです。

結論

Java のパフォーマンスチューニングにおいては,直感はしばしば誤信の元になります。 プラットフォームの動作を視覚化し理解するには,実証的なデータとツールが必要です。

ガベージコレクションは,おそらくこの最高の実例でしょう。GC サブシステムはチューニングの面でも,チューニングに必要な情報生成という面においても,驚くべき可能性を秘めています。しかし実運用アプリケーションにおいては,ツールに頼らずに生成されたデータの意味を理解することは非常に困難です。

どのようなJavaプロセスを実行 (開発も実運用も) する場合,最低限でも以下のフラグは常に設定するべきです。
-verbose:gc (GC ログの出力)
-Xloggc: (より包括的な GC ログ出力)
-XX:+PrintGCDetails (詳細な出力用)
-XX:+PrintTenuringDistribution (JVM が Tenured として扱うしきい値を表示する)

次にツールを使用して生成されたログを解析します。ツールは手書きスクリプトやグラフ生成ツールの他,(オープンソースの) GCViewer や jClarity Censum といった視覚化ツールもあります。

著者について

Ben Evans 氏は jClarity の CEO です。同社は開発者や運用チーム支援のパフォーマンスツールを提供する新興企業です。LJC (London JUG) の創設者もあり,JCP Executive Committee のメンバとして Java エコシステムの標準作成を支援してもいます。Java Champion,JavaOne Rockstar,"The Well-Grounded Java Developer" の共同著者でもある氏は,Java プラットフォームやパフォーマンス,並列性,その他のトピックに関して定期的に講演を行っています。

 

 

この記事に星をつける

おすすめ度
スタイル

BT