なみひらブログ

学んだことを日々記録する。~ since 2012/06/24 ~

パフォーマンスを意識したJavaコーディング

この記事はJava Advent Calendar 2015 - Qiitaの9日目の記事です。

8日目の記事:JavaでHttpのGETとかPOSTをさくっと実装したい - Qiita
10日目の記事:JavaFXのUIをJUnit形式でテストする - Katsumi Kokuzawa's Blog

背景

以下の本を社内の読書会で読んで、「結局パフォーマンスを意識したコードはどう書くんだっけ?( ゚д゚ )ガタッ」となったので、ちょっとメモっときます。
Javaパフォーマンス

Javaパフォーマンス

注).以下の(p.x)は書籍内のページ番号。
注).自分の解釈(間違っている可能性あり)も書いているので、正しい情報が必要ならば上記の書籍を参照してください(;´Д`)

全体

大方針としては以下で、パフォーマンスを意識してコーディング(と測定)する際に意識する必要がある。

  • よりよいアルゴリズムを記述する(p.6)
  • コードの量を少なくする(p.6)
  • 早まった安易な最適化は避ける(p.7)
  • 外部を見渡す(データベースは常にボトルネック)(p.9)
  • よくあるケースのために最適化する(p.10)

以下でそれぞれの説明と自分の理解を記載しておきます。

よりよいアルゴリズムを記述する

(p.6)
最終的には、アプリケーションのパフォーマンスはコードの適切さにかかっています。
(中略)
このループの目的が特定の要素の発見することだとしたら、配列ベースのコードを最適化してもHashMapを使う場合のパフォーマンスにはかないません。

これは前提の前提で、アルゴリズムは良いものを選ばないといけない(;´Д`)レビューしなければいけない。
アルゴリズム(○○ソートとか)とデータ構造(ListとかMapとか)については以下を参照すべき。

プログラミング作法

プログラミング作法

コードの量を少なくする

コード量(クラスとかメソッドとか)は少ないことに越したことはない。多い(大きい)と以下のような弊害がある。

  • JITコンパイル(後述)による最適化が遅れ、パフォーマンスが最適になるまでに時間がかかる。
  • 大きいオブジェクトがメモリ上に配置されると、GCが頻発したりGC実行時間が長くなりパフォーマンスが落ちる。
  • さらに大きいオブジェクトの場合(例:巨大なバイナリをもっているオブジェクトとか)は、メモリに収まらずディスクに配置されアクセスの際にページングが発生してしまう(パフォーマンスが落ちる)。

方針としては

  • 不要なもの(クラス、メソッド)はどんどん削除していったほうがいい。クラスは小さく保つ。
    • メソッドはよく足すけど、削除は意識してしたことがない(;´Д`)privateメソッドの整理とか。

(p.7)
もちろん、製品に新機能や新しいコードを追加するなというつもりはありません。これらはプログラムの強化につながり有益です。行おうとして変更が生むトレードオフを意識し、可能なかぎり簡素化を心がけましょう。

了解です(;´Д`)

早まった安易な最適化

(p.8)
「1日のうち97%くらいは、ささいな効率については忘れるべきです。早まった最適化は諸悪の根源です」とされています。
 この格言のポイントは、最終的にはクリーンで率直なコードを記述し、読みやすく理解もしやすいようにするべきという点です。(中略)この種の最適化は、(例えば)プロファイリングによって大きなメリットが示されるまではしないままでおくべきです。

方針としては

  • よく言う「憶測するな。計測せよ」といったように効果が分かるまでは過剰な最適化(コード改修)は避けたほうがいい。
  • パフォーマンスに意識してコード(アルゴリズム)を意図的な(違和感のある)ものにする場合は、コメントが一筆要る。

また、以下のようにコーディング中に完全にパフォーマンスのことを意識しないかというとそうでもない。

(p.8)
一方、パフォーマンスに悪影響を与えるとわかっているコードを避けるというのは別の問題であり、望ましい最適化です。

例えば以下のようなケースを考えると、

logger.info("X : " + calcX() + ",Y : " + calcY());

上記のコードは、ログレベルに関わらず引数の計算が実行されるため、それが冗長な処理になる(infoレベルより低いときは利用されないのに。)。
こういうことが直感的にわかった場合*1は、例えば以下のような最適化が実施すべきだということ。
#Lambda式は実行が遅延され必要なときに評価されることを利用している。

logger.info(() -> "X : " + calcX() + ",Y : " + calcY());

外部を見渡す(データベースは常にボトルネック

アプリケーション自体がどんなに最適化されても、外部モジュール(フロントエンドやバックエンドのサーバや、DBやネットワークなどのI/O関連)の影響の場合もある。
あるときには性能測定に使っているツール側による見せかけの劣化の可能性もある*2。コード自体ばかりに目をむけるのでなくて、システム全体でパフォーマンスを把握することが大切。

方針としては

  • これも実際に測定してみてボトルネックがあるとわかった時点で対応するのが効率的で効果的。
  • 各モジュール間でざっくり式を組み立ててみると検討しやすい。実際の値はわからなくても各処理区間での相対的な処理速度を把握しておくことが大切。

よくあるケースのために最適化する

あまり実行されない部分よりも、最も実行されるシーケンスについて改善するほうが効果が大きい。

JITコンパイルJavaコーディング

[復習]JITコンパイル

JVMはコードをJavaバイトコードとして実行しているが、頻繁に実行されるコードを各環境(CPUとかOSとか)に最適化された機械語に変換(コンパイル)することで動作の高速化をしてくれる。

インライン化

JITコンパイルで最も重要なものがメソッドのインライン化。
例えば、以下のようなクラスがあって
(p.103)

public class Point {
    private x;

    public void getX(){ return x;}
    public void setX(int i) { x = i; }
}

以下のようなコードを書いた場合

Point p = getPoint();
p.setX(p.getX() * 2)

JITコンパイルされると以下のようになる

 Point p = getPoint();
 p.x = p.x * 2;

オブジェクトを経由した場合、メソッドの探索自体やオーバーライドを考慮しないといけないため処理コストが高くなる。JITコンパイラをその辺の最適化をやってくれる。
メソッドをインライン化する条件は以下の2つ。 

  • メソッドのサイズ
    • デフォルト325バイト(または-XX:MaxFreqInlineSize=Nで指定できる)以下であること。・・・160字ぐらいメソッド?(;´Д`)
    • 35バイト(または-XX:MaxInlineSize=Nで指定できる)以下は無条件でインライン化される。
  • メソッド実行の頻度
    • 条件については明示的な仕様はない

対応方針
MaxFreqInlineSizeオプションで上限をあげても不要なメソッドがインライン化されるだけで効果が薄いので、メソッドをできるだけ分割して短く書いてあげたほうがJVMにとって優しい。

finalキーワード

(p.110)
一部の人々は、JITコンパイラがインライン化などの最適化を行う際に役立つためにfinalは重要だと考えています。(中略)しかしこのような考えは遠い過去のものであり、現在ではまったく正しくありません。(中略)このキーワードがあってもパフォーマンスには影響しません

そうだったのか(;´Д`)

ガベージコレクションJavaコーディング

メモリ使用量を減らす

ヒープメモリの消費を軽減すると、GCの実行回数(young領域へのスキャン、survivor空間との往復、old領域への昇格などなど)が減り、パフォーマンスの安定が見込まれる。
メモリ使用量を減らす方針は以下の通り。

  • オブジェクトのサイズを減らす
    • 対応例
      • インスタンス変数の数を減らす。
        • 2次成果物的な変数(aが決まるとbが決まるようなもの)は避ける。
      • インスタンス変数のサイズを減らす。
        • よりサイズの小さい型で宣言する。(この対応は無理がでるのでオススメしない)
  • Java(に限らないけど)の場合のGCは「短寿命のオブジェクトはすぐに不要になり、長寿命のオブジェクトはずっと必要」という前提でGCが行われるので、できる限りにそれに沿ったライフサイクルにしておいたほうがいい。

オブジェクトの初期化を遅らせる

オブジェクト内で別の大きなオブジェクトをもつ必要がある場合、その大きなオブジェクトを定常的に利用しないのであればその大きなオブジェクトの初期化を遅らせるとパフォーマンス向上に効果がある。

例えば、以下のようなコード。Calendarオブジェクトは生成のコストが大きい。
(p.205)

public class CalDateInitialization {
    private Calendar calendar = Calendar.getInstance();
    private DateFormat df = DateFormat.getDateInstance();

    private void report(Writer w) {
        w.write("日付: " + df.format8calendar.getTime()) + ": " + this);
    }
}

このようなクラスはnewする度にCalendarも作られる(Calendarオブジェクトを使わない場合でも)。
なので以下のように初期化を遅延させると生成コストが小さくなり、リソースを節約できる。
(p.205)

public class CalDateInitialization {
    private Calendar calendar;
    private DateFormat df;

    private void report(Writer w) {
        if (calendar == null) {
            calendar = Calendar.getInstance();
            df = DateFormat.getDateInstance();
        }
        w.write("日付: " + df.format8calendar.getTime()) + ": " + this);
    }
}

おわりに

  • 備忘録的にまとめたけど、長くなったのでとりあえずおわり(;´Д`)
  • あとネットワークプログラミング関連、DBプログラミング関連は別途まとめたい。

参考

*1: 経験がものをいう(;´Д`)

*2: 以前「遅くなっている」と思ったら、ツールを動かしているPCのCPUが先に頭打ちになっていたことがある(;´Д`)