Designing for Performance
- ここまで(1-19章)のソフトウェア設計の議論は、複雑性に焦点を当てていた
- 目標は、可能な限り単純で理解しやすいソフトウェアを作ることだった
- 高速に動作する必要のあるシステムではどうか?
- パフォーマンスを考慮すると設計プロセスはどのような影響を受けるか?
-
本章のテーマ: 綺麗な設計を犠牲にすることなく高パフォーマンスを達成する
-
最重要アイデアはやはり単純さ
- システムの設計を向上させるのみならず、システムを高速にする
-
How to think about performance
- 「普段の開発プロセスで、どれくらいパフォーマンスに気を配っていますか?」
-
両極端
-
全ステートメントをカリカリにチューニング
- 開発は遅くなる
- 不要な複雑性を導入してしまう
- 「最適化」の多くは実際にはパフォーマンスに寄与しない
-
パフォーマンス問題完全無視
- コード全体に、看過できない非効率が大量に広がる
- いとも簡単に、要件よりも5-10倍も遅くなってしまう
- チリツモなので後から改善するのが困難
-
-
ベストはこの間のどこか
- 「自然に効率的」で、かつ綺麗で単純な選択を
-
本質的に高コストな処理を押さえて開発するのが鍵
- ネットワーク通信
- 副記憶装置のI/O
- 動的メモリ割り当て
- キャッシュミス
-
何が高コストか知るには、ベンチマークをとるのが最良
- ベンチマークフレームワークは数日程度で作れる
- RAMCloudの開発時にお世話になった
- 処理のコスト感を掴んだら、それを元に低コストな処理を選ぶ
-
多くの場合、同程度に単純だが、より効率的な処理というものが存在する
-
例: Hash vs OrderedMap
- 【補】ハッシュテーブルはO(1)
- 【補】順序木はO(log(n))
-
例: メモリ割り当て
-
構造体のポインタの配列
-
割り当てが複数回必要
- ポインタの配列の割り当て
- 個々の構造体の割り当て
-
-
構造体の配列
- 一撃で割り当てできる
- 【補】ポインタ分の空間も節約できる
- 【補】derefも不要
-
-
-
パフォーマンス改善のために複雑性を増すほかない場合
- 難しい選択
-
複雑性が少量で、かつ複雑性を隠蔽できる場合
- やる価値あり
- ただし、複雑性は積み重ねであることに留意せよ
-
複雑性が大量、またはインタフェースが複雑になる場合
- まず単純なアプローチで始めるのがよい
- あとでパフォーマンスが問題になったら最適化する
-
パフォーマンスが問題になるという明確な証拠があるなら、最初から高速な実装を選ぶほうがよい
- 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
- 無関係な、異物の