パフォーマンス改善の時に考えること(プログラムレベル)
パフォーマンス改善について
最近作成しているシステムはデータを扱うケースが多く、処理を書くたびにパフォーマンスが問題になっています。扱うデータが10万を超えて、全てインメモリという条件がついているのでなかなか厳しいです。
今回はこのパフォーマンス改善についてとり組んできたことについて書きます。
パフォーマンス改善には、レスポンス改善とメモリ改善の両方があります。今回はどちらも取り組んだのでその成果について書きます。
ただし、今回はプログラムレベルできる改善にとどめます。今回は既存DBも扱うシステムで、DB周りはいじれませんでした。
両方に共通すること
- 目標を決める
- 計測する
- テストをする
目標を決める
一番重要なことはここまでのパフォーマンスは必要というラインを決めることだと思います。それによって取れる対策が変わってきます。
本当に一瞬で返すことが必須要件といわれるとアーキテクチャをある程度壊さないといけないですが、数秒程度でOKならそこまでしたくはないです。
あとは、画面によって求められる速度は違います。よく使う画面ほど速度が大事なので、そこらへんの見極めもありますね。
なんにせよ、目標がなければパフォーマンス改善はできません。
また、環境条件もきちんと決めておいたほうがいいと思いますね。
使えるメモリの量、ネットワークの速度を考えなかった場合、結果が異なることがあります。
そして、環境のほうを改善してもらえるのなら、できる限りプログラムは触らないほうがいいと思います。技術的負債よりも環境を変えるほうが安いです。
計測する
ボトルネックとなっている箇所を見極めることは大事です。
アーキテクチャの大きな変更を伴う場合はある程度決め打ちすることもありますが、大体は計測してボトルネックだけを治して最小限の変更でとどめたいです。
print文で計測することもありますが、最近はVS2015に「パフォーマンスプロファイラー」という機能があるのでそちらを優先的に使っています。
やはり、本気で作業する場合は手動よりも機械的な方法に頼るほうがいいですね。
テストをする
当たり前ですが、パフォーマンス改善で機能を変えてはだめです。
といっても大きなアーキテクチャの変更を伴う場合はこれが一番難しいです。
テストスイートでもあればいいのですが、今回はなかったのでDBの最終結果をSQLで比較していました。
とにかく小さな修正をするたびにテストしたほうがいいと思います。
レスポンス改善
- ループ回数を減らす。
- インメモリにする
- 非同期にする。
ループ回数を減らす。
10万件のループになると、それだけで遅いです。仮に処理が簡単であっても一気にボトルネックになります。
SQLで何とかする
ではどうするのかというと、まずはSQLでなんとかできないかを考えます。
ビジネスロジックをSQLに持ちこむべきではないとは思いますが、パフォーマンスの観点からいえばできる限りDBサーバーに働いてもらったほうが効率がいいです。
特に取得・集約処理を1回のSQLでできるようにすれば、大分早くなるのでこれをまず検討します。特に階層系のSQLをまとめると大きな改善になります。
以前、Join禁止というプロジェクトがありましたが、検索のレスポンスが1分とか5分とかでした。やはり、SQLを最大限に使わずパフォーマンスを出すのは難しい面があります。
また、自分たちの使っているDBはOracleですが、一括でSQLを発行するBulk機能があります。これを使うと何万件の命令をすぐにこなせたのでなかなか良かったです。
参照関係をきちんともつ
今回のシステムではあるコレクションとあるコレクションをキーでぶつけて、処理をするということを何度もやっていました。もちろん、ループで。
こういうキーでぶつけれるときは、データ構造に参照関係をもつとループが減ります。
どうしても、データ構造を変えれないときはLinqのJoinなどで乗り切ってもいいと思います。
インメモリにする
特にIOがネックになっている時は、インメモリで処理します。
データベースはうまく仕事を任せると早いですが、回数が多いと逆に最大のボトルネックになります。
見極めは難しいですが、難しいビジネスロジックが入ってくる場合はインメモリで処理すると早いです。
うちでは些細なことですが更新日付やマスタ値をとってくるところを数1000回ループして、遅くなることがありますね。
こういうDBアクセスはとりあえずメモリに入れてしまうという手があります。
メモリ使用量との兼ね合いもありますけどね。
非同期にする
最近はLinqのAsParallelやTaskがあるので非同期のハードルは低いです。
処理の順番に制約がなく、メイン処理に関係ないところでは非同期でやってしまうという手があります。
ただ、個人的にはバグになりやすい部分もあるのでできる限り最終手段にしています。
最初からそういうデータ構造にしておくのならいいのですが。
メモリ改善
- 都度処理を行う
- データ型を変える
都度処理を行う
例えば普段かくプログラムはいったんDBから読んだレコードをリストに入れて、リストに対して処理するように書きます。
ただ、あまりにもレコードが多いときは1件ずつ読みながら、その都度処理をしてクラスに持たないようにします。
実際問題、これができるケースは多くはないです。
またデータを極力DBに入れて、必要な時に必要な分だけとってくるようにします。
とくに表示するためのデータなどはインメモリで持たないようにします。レスポンス的には遅くなるので難しいですが、兼ね合いで決めます。
データ型を変える
最終手段的なところはありますが、数字を扱う型をdecimalからdoubleやintに変えると何10%かは軽くなったと思います。
規約とシステムにもよりますが、数値の型はメモリに影響が大きいようです。
まとめ
今回行った対策はこのような感じです。10万件扱えるアーキテクチャと数千件のデータを扱うアーキテクチャの違いが分からず、結構苦戦しています。
.NETのネイティブ周りや独自仕様(Formaアプリケーション)に詳しくないも原因かなと思います。
そこらへんにもいずれ踏み込んで勉強したいです。