Javaでパフォーマンスを意識した開発を行っていく上での心得

仕事でパフォーマンス要件の厳しいJavaアプリケーションを開発することになったので、
いくつかの書籍や実体験をもとに肝に銘じておくべきことをまとめてみます。

なお、この記事で出てくる「テスト」は、特に注記がない限り「パフォーマンステスト」の指します。

小手先のチューニングのために「コードの読みやすさ」を犠牲にしない

「パフォーマンスのためならば可読性を削ってでも最適化された設計をしていくべき」
これは良くない考え方です。
考えられる例としては、「クラスファイルを可能な限り減らす」「何でもstaticとして定義する」「変数名をとにかく短いものにする」といったことでしょうか。

Donald E. Knuth先生の格言として次のようなものがあります。
「わずかな効率、たとえば時間の約97%については忘れるべきである。時期尚早の最適化は、すべての悪の根源である。」
ここでの最適化は「コードを複雑化させ、読みやすさを犠牲にしつつパフォーマンスを生み出すもの」と解釈します。
何でもすぐに最適化に手を付けるのではなく、実際にテスト・計測を実施して明らかにボトルネックになるとわかったそのとき初めて検討しましょう。

誤った最適化によるコードは、その後修正しにくい形で残ってしまうでしょう。俗に(厳密な定義はさておき)技術的負債とも呼ばれます。
パフォーマンスのためと括って書いたコードに、後々返り討ちに遭うことになるかもしれないので要注意です。

事実ベースでチューニングを行う

前項と重複しますが、テストの測定結果、プロファイリングなどの事実を根拠にチューニングを実施しましょう。
憶測だけでチューニングを行っても、期待した結果が得られないかもしれません。

Javaの場合、優秀なJITコンパイラによって頻繁に実行される箇所が最適化されていきます。(HotSpot JVMを利用した想定です)
「ここがパフォーマンス悪化の原因!」と直してみた箇所も、実はJITコンパイラの最適化結果と変わらない、むしろ悪化するかもしれません。
VisualVMといったツールを用いてプロファイリングを実施し、パフォーマンス悪化した部分を特定して改善することが効果的です。

可能な限り実環境でテストをする

実際のアプリケーションを使い、実際に動く環境で、実際の使い方でテストを行いましょう。
場合によっては難しいかもしれませんが、可能な限り近い条件でテストすべきです。

テストの分類として、マイクロベンチマークというものがあります。
これはアプリケーションのコードの一部など、ユニットテストぐらいの手軽さのテストです。
JavaだとJMHというライブラリが有名のようです。

もちろんここれでもコードの処理が速い/遅いをチェックできますが、あくまで参考値です。
実際のアプリケーションではCPU、メモリ、ネットワーク、I/Oなどなど別に問題があるかもしれません。
マイクロベンチマークの優先順位としては後にしておき、まずは全体は実環境に近いテスト環境を整えたほうが良いでしょう。

複数のパフォーマンスの観点を考慮し、目標を設定する

一口にパフォーマンスといっても、指標は様々です。例を挙げると、

  • スループット … 一定時間内にどれだけのリクエストを処理できるか?
  • レスポンスタイムの平均値 … 平均でどれだけの時間で返却するか?
  • レスポンスタイムのパーセンタイル値 … 99%のリクエストのうち最も遅いのは?(1%の外れ値は?)

このようなものがあります。
要件によって目標値は違ってくると思います。テストを実施する前に、予め目標を決め、それにあった指標を取ることを忘れないようにしましょう。

テスト結果のブレに注意

全く同じアプリケーションで全く同じリクエストであっても、実行のたびに結果が異なることがあります。
動作環境の別プロセスの影響、ネットワークの混み具合など理由は様々です。

このブレを認識し、統計的な有意さを求めるためにt検定といったアプローチを取ることも可能です。
しかし、結果を求めるためにテストを複数回行うなど、現実的でない・手間がかかるといったことが考えられるでしょう。

大切なのは、このブレについて認識しておくことです。
ある程度の結果のブレを認めた上で、改善のためにコストをかけましょう。

早期から頻繁にテストを実施する

開発サイクルの中にパフォーマンステストを組み込むことが理想です。
常にテストを実施することで、「どの変更によりどう影響が出たのか」明確になり、早期に修正も可能となります。

開発サイクルに組み込む場合、自動化することが理想です。
CIパイプラインを利用している場合、デプロイ前or後にテスト用ジョブを設けると良いでしょう。

また、テスト時の測定値は分析に使えるものはすべて収集しておくことが望ましいです。
レスポンスタイム平均・パーセンタイル値はもちろん、実行環境のスペック、プロファイルデータなどアプリケーションに関する情報についてもです。

---

以下はJava(もしくはJVM言語)特有の話となります。
様々な最適化テクニックを行う前の、前提として心がけるべき点だけ挙げます。

ウォームアップを必ず実施する

Javaを動かす実装は、多くの場合HotSpot JVMと想定されます。
このJVMはホットな箇所、つまり何度も実行される部分のみをコンパイルし最適化していきます。
これはビルド時や初回リクエスト時ではなく、何度もリクエストされながら適宜行われます。
(「最初は舗装されてない道だらけだが、たくさん車が通る道を見つけては舗装する」というイメージ)

よって、起動直後のJavaアプリケーションは最適化されておらず、十分なパフォーマンスは出ないでしょう。
そのためにウォームアップ(暖機運転)を実施すべきです。

ウォームアップのやり方は簡単で、起動後に想定されるリクエストを何百~何千回か投げればOKです。
パフォーマンス計測時はもちろん、サービスインの直前に行っておくのが理想です。

具体的なプラクティスは次の記事が参考になります。(kubernetesで動かす前提の記事ですが。。)


闇雲にJVMパラメータを設定しない

ネット上や書籍を漁ると、「このオプションは設定すべき」「この値はこれくらいがベスト」といった多くのプラクティスが見つかります。
どのオプションを使うor使わないの判断は、必ず根拠をもって行うようにしましょう。

オプションによっては、バージョンが変わったことによりデフォルトでも効くようになっていることもあります。
メジャーバージョンだけでなく、マイナーバージョンアップでもその可能性はあります。
例えば、Docker上でCPU/メモリを正しくホストを認識させる設定-XX:+UseContainerSupportは、同じJava 8の8u191以降で不要となりました。
本当に設定すべきか情報収集を行ってから対応しましょう。

また、JVMオプションを追加した場合はその根拠もどこかに残しておくべきです。
さもないと後々「なぜ設定されてるかわからないけど外そうにも外せない」ということになってしまいます。
後世のためにもコメントやドキュメントに残しておきましょう。

参考資料

Javaパフォーマンス

Javaのパフォーマンスチューニングに関して、原則から測定方法、JVMチューニングやコードのプラクティスまで幅広く扱われています。
いくつかのGCのアルゴリズムについてもこの書籍で学べます。
バージョンはJava 7,8に基づいて書かれています。

なお、本記事はこの書籍の第1章、第2章をベースにしております。

Effective Java 第3版

言わずとしれた名著です。
パフォーマンスに関して特に関係があるのは「項目67 注意して最適化する」が挙げられます。他にも最適化に関する内容はいくつか見られます。
バージョンはJava 9に基づいて書かれています。