A Philosophy of Software Design ch20 Designing for Performance
Designing for Performance
- ここまで(1-19章)のソフトウェア設計の議論は、複雑性に焦点を当てていた
- 目標は、可能な限り単純で理解しやすいソフトウェアを作ることだった
- 高速に動作する必要のあるシステムではどうか?
- パフォーマンスを考慮すると設計プロセスはどのような影響を受けるか?
- 本章のテーマ: 綺麗な設計を犠牲にすることなく高パフォーマンスを達成する
- 最重要アイデアはやはり単純さ
- システムの設計を向上させるのみならず、システムを高速にする
- 最重要アイデアはやはり単純さ
How to think about performance
- 「普段の開発プロセスで、どれくらいパフォーマンスに気を配っていますか?」
- 両極端
- ベストはこの間のどこか
- 「自然に効率的」で、かつ綺麗で単純な選択を
- 本質的に高コストな処理を押さえて開発するのが鍵
- ネットワーク通信
- 副記憶装置のI/O
- 動的メモリ割り当て
- キャッシュミス
- 何が高コストか知るには、ベンチマークをとるのが最良
- 処理のコスト感を掴んだら、それを元に低コストな処理を選ぶ
- 多くの場合、同程度に単純だが、より効率的な処理というものが存在する
- 例: Hash vs OrderedMap
- 【補】ハッシュテーブルはO(1)
- 【補】順序木はO(log(n))
- 例: メモリ割り当て
- 構造体のポインタの配列
- 割り当てが複数回必要
- ポインタの配列の割り当て
- 個々の構造体の割り当て
- 割り当てが複数回必要
- 構造体の配列
- 一撃で割り当てできる
- 【補】ポインタ分の空間も節約できる
- 【補】derefも不要
- 構造体のポインタの配列
- 例: Hash vs OrderedMap
- パフォーマンス改善のために複雑性を増すほかない場合
- 難しい選択
- 複雑性が少量で、かつ複雑性を隠蔽できる場合
- やる価値あり
- ただし、複雑性は積み重ねであることに留意せよ
- 複雑性が大量、またはインタフェースが複雑になる場合
- まず単純なアプローチで始めるのがよい
- あとでパフォーマンスが問題になったら最適化する
- パフォーマンスが問題になるという明確な証拠があるなら、最初から高速な実装を選ぶほうがよい
- RAMCloudの例: カーネルベースのネットワーク通信では遅すぎることが最初からわかっていた
- ネットワーク通信に「アタリ」をつけることで、他のほとんどの部分の設計は単純にできた
- 一般論として、単純なコードは高速に動作する傾向にある
- 特殊ケースや例外を定義しないほうが高速
- チェックコードも不要になるため
- 深いクラスは浅いクラスよりも効率的
- メソッドコールあたりの仕事量が多い
- 仕事量あたりのメソッドコールを減らせる = オーバヘッドを減らせる
- 特殊ケースや例外を定義しないほうが高速
Measure before modifying
- 上述のような設計をしてなお、システムの速度が不十分だったとする
- 何が遅いか当て推量パフォーマンスチューニングにかかりたい衝動にかられる
- これはいけない
- パフォーマンスに関して、プログラマの直感は当てにならない
- 熟練したプログラマにおいても例外ではない
- 時間を無駄にし、パフォーマンスは改善せず、システムはより複雑なってしまうのがオチ
- まずシステムの既存のふるまいを測定せよ
- 2つの目的がある
Design around the critical path
- それでもなお遅い部分がある場合は?
- 根本的な変更を加える
- キャッシュの導入
- アルゴリズムを変える
- 平衡木 vs リストとか
- それでもダメなら?
- 本章の核心となる課題: 既存のコード片を再設計して、より高速動作するようにする
- 最後の手段
- 考え方の鍵は、クリフィカルパス周りのコードを設計すること
- 本章の核心となる課題: 既存のコード片を再設計して、より高速動作するようにする
- まず、最も一般的なケースで実行される最小のコードは何か考える
- 既存の構造は無視
- 特殊ケースは無視
- 複数のメソッドコールも無視し、単一のメソッドコールで実行されるものとする
- クリティカルパスで最も便利なデータ構造のみ考える
- 例えば、複数の変数を1つにまとめたほうが便利かもしれない
- これを「理想形」とする
- 既存のクラス構造と相容れないかもしれない
- 現実的でないかもしれない
- が、よい目標にはなる
- 最も単純で速い上限
- 【補】RTAに対するTASみたいな
- 続いて、綺麗な構造を保てる範囲で、最も理想形に近い設計を模索する
- 本章1-19で論じてきた設計のアイデアを適用する
- ただし、「ほぼ理想形のまま保つ」という制約条件がある
- 綺麗な抽象化のために余計のコードを追加したりするのはOK
- 汎用のハッシュテーブルクラスを切り出して呼び出すなど
- 最も重要なのは、クリティカルパスから特殊ケースを取り除くこと
- 遅いコードの理由の多くは、あらゆるケースをシンプルに処理するためにコードを構造化していること
- 特殊ケースごとに、クリティカルパスに条件分岐やメソッドコールが追加される
- 理想的には、処理冒頭の単一のif文で、特殊ケースすべてを検出できるはずである
- 通常ケースではこれ以降一切の分岐が不要
- 特殊ケースでのみ、クリフィカルパスとは別の部分で、さらに分岐しうる
- 特殊ケースではパフォーマンスはあまり重要でないので、単純さ重視で設計できる
- 遅いコードの理由の多くは、あらゆるケースをシンプルに処理するためにコードを構造化していること
An example: RAMCloud Buffers
- (すごく具体例なので略)
- 結果
- 2倍の高速化
- 設計の単純化
- コード量20%削減
Conclusion
- 本章で一番伝えたかったことは、綺麗な設計と高パフォーマンスとは両立可能だということ
- 込み入ったコードは遅くなりがち
- 余計・冗長な動作があるため
- 綺麗で単純なコードを書けば、システムは十分な速度を発揮することが多い
- パフォーマンス最適化が必要になるケースは滅多にない
- 滅多にないその場合でも、鍵となるのは単純さ
英語
- death by a thousand of cuts
- 何千もの切り傷をつけて死ぬようなやり方
- チリツモ的な意味
- 何千もの切り傷をつけて死ぬようなやり方
- resort
- 最後の手段
- intact
- 手を付けていなくて、無傷で
- compatible
- 両立可能
- extraneous
- 無関係な、異物の