勉強日記

チラ裏

A Philosophy of Software Design ch20 Designing for Performance

www.goodreads.com


Designing for Performance

  • ここまで(1-19章)のソフトウェア設計の議論は、複雑性に焦点を当てていた
  • 目標は、可能な限り単純で理解しやすいソフトウェアを作ることだった
  • 高速に動作する必要のあるシステムではどうか?
  • パフォーマンスを考慮すると設計プロセスはどのような影響を受けるか?
  • 本章のテーマ: 綺麗な設計を犠牲にすることなく高パフォーマンスを達成する
    • 最重要アイデアはやはり単純さ
      • システムの設計を向上させるのみならず、システムを高速にする

How to think about performance

  • 「普段の開発プロセスで、どれくらいパフォーマンスに気を配っていますか?」
  • 両極端
    • ステートメントカリカリにチューニング
      • 開発は遅くなる
      • 不要な複雑性を導入してしまう
      • 「最適化」の多くは実際にはパフォーマンスに寄与しない
    • パフォーマンス問題完全無視
      • コード全体に、看過できない非効率が大量に広がる
      • いとも簡単に、要件よりも5-10倍も遅くなってしまう
      • チリツモなので後から改善するのが困難
  • ベストはこの間のどこか
    • 「自然に効率的」で、かつ綺麗で単純な選択を
  • 本質的に高コストな処理を押さえて開発するのが鍵
    • ネットワーク通信
    • 副記憶装置のI/O
    • 動的メモリ割り当て
    • キャッシュミス
  • 何が高コストか知るには、ベンチマークをとるのが最良
  • 処理のコスト感を掴んだら、それを元に低コストな処理を選ぶ
  • 多くの場合、同程度に単純だが、より効率的な処理というものが存在する
    • 例: Hash vs OrderedMap
      • 【補】ハッシュテーブルはO(1)
      • 【補】順序木はO(log(n))
    • 例: メモリ割り当て
      • 構造体のポインタの配列
        • 割り当てが複数回必要
          1. ポインタの配列の割り当て
          2. 個々の構造体の割り当て
      • 構造体の配列
        • 一撃で割り当てできる
        • 【補】ポインタ分の空間も節約できる
        • 【補】derefも不要
  • パフォーマンス改善のために複雑性を増すほかない場合
    • 難しい選択
    • 複雑性が少量で、かつ複雑性を隠蔽できる場合
      • やる価値あり
      • ただし、複雑性は積み重ねであることに留意せよ
    • 複雑性が大量、またはインタフェースが複雑になる場合
      • まず単純なアプローチで始めるのがよい
      • あとでパフォーマンスが問題になったら最適化する
      • パフォーマンスが問題になるという明確な証拠があるなら、最初から高速な実装を選ぶほうがよい
        • RAMCloudの例: カーネルベースのネットワーク通信では遅すぎることが最初からわかっていた
        • ネットワーク通信に「アタリ」をつけることで、他のほとんどの部分の設計は単純にできた
  • 一般論として、単純なコードは高速に動作する傾向にある
    • 特殊ケースや例外を定義しないほうが高速
      • チェックコードも不要になるため
    • 深いクラスは浅いクラスよりも効率的
      • メソッドコールあたりの仕事量が多い
      • 仕事量あたりのメソッドコールを減らせる = オーバヘッドを減らせる

Measure before modifying

  • 上述のような設計をしてなお、システムの速度が不十分だったとする
  • 何が遅いか当て推量パフォーマンスチューニングにかかりたい衝動にかられる
  • これはいけない
  • パフォーマンスに関して、プログラマの直感は当てにならない
    • 熟練したプログラマにおいても例外ではない
    • 時間を無駄にし、パフォーマンスは改善せず、システムはより複雑なってしまうのがオチ
  • まずシステムの既存のふるまいを測定せよ
  • 2つの目的がある
    • パフォーマンスに最も影響を与えている部分を探る
      • トップレベルの測定では不十分
        • システムが遅いことの確認にしかならない
      • より深く、要素ごとに測定する必要がある
    • 基準値を与える
      • 変更を加えたら再測定し、パフォーマンスが本当に改善したことを確認する
        • 有意な差が見られなければ変更を巻き戻す
          • (システムが単純にならない限り)
          • 速くならないなら複雑性を抱える必要はない

Design around the critical path

  • それでもなお遅い部分がある場合は?
  • 根本的な変更を加える
  • それでもダメなら?
    • 本章の核心となる課題: 既存のコード片を再設計して、より高速動作するようにする
      • 最後の手段
    • 考え方の鍵は、クリフィカルパス周りのコードを設計すること
  • まず、最も一般的なケースで実行される最小のコードは何か考える
    • 既存の構造は無視
    • 特殊ケースは無視
    • 複数のメソッドコールも無視し、単一のメソッドコールで実行されるものとする
    • クリティカルパスで最も便利なデータ構造のみ考える
      • 例えば、複数の変数を1つにまとめたほうが便利かもしれない
  • これを「理想形」とする
    • 既存のクラス構造と相容れないかもしれない
    • 現実的でないかもしれない
    • が、よい目標にはなる
      • 最も単純で速い上限
      • 【補】RTAに対するTASみたいな
  • 続いて、綺麗な構造を保てる範囲で、最も理想形に近い設計を模索する
    • 本章1-19で論じてきた設計のアイデアを適用する
    • ただし、「ほぼ理想形のまま保つ」という制約条件がある
    • 綺麗な抽象化のために余計のコードを追加したりするのはOK
      • 汎用のハッシュテーブルクラスを切り出して呼び出すなど
  • 最も重要なのは、クリティカルパスから特殊ケースを取り除くこと
    • 遅いコードの理由の多くは、あらゆるケースをシンプルに処理するためにコードを構造化していること
    • 理想的には、処理冒頭の単一のif文で、特殊ケースすべてを検出できるはずである
      • 通常ケースではこれ以降一切の分岐が不要
      • 特殊ケースでのみ、クリフィカルパスとは別の部分で、さらに分岐しうる
        • 特殊ケースではパフォーマンスはあまり重要でないので、単純さ重視で設計できる

An example: RAMCloud Buffers

  • (すごく具体例なので略)
  • 結果
    • 2倍の高速化
    • 設計の単純化
    • コード量20%削減

Conclusion

  • 本章で一番伝えたかったことは、綺麗な設計と高パフォーマンスとは両立可能だということ
  • 込み入ったコードは遅くなりがち
    • 余計・冗長な動作があるため
  • 綺麗で単純なコードを書けば、システムは十分な速度を発揮することが多い
  • パフォーマンス最適化が必要になるケースは滅多にない
  • 滅多にないその場合でも、鍵となるのは単純さ

英語

  • death by a thousand of cuts
    • 何千もの切り傷をつけて死ぬようなやり方
      • チリツモ的な意味
  • resort
    • 最後の手段
  • intact
    • 手を付けていなくて、無傷で
  • compatible
    • 両立可能
  • extraneous
    • 無関係な、異物の