A Philosophy of Software Design ch20 Designing for Performance

APoSDソフトウェア設計勉強メモ

出典: 


Designing for Performance

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

    • 最重要アイデアはやはり単純さ

      • システムの設計を向上させるのみならず、システムを高速にする

How to think about performance

  • 「普段の開発プロセスで、どれくらいパフォーマンスに気を配っていますか?」
  • 両極端

    • 全ステートメントをカリカリにチューニング

      • 開発は遅くなる
      • 不要な複雑性を導入してしまう
      • 「最適化」の多くは実際にはパフォーマンスに寄与しない
    • パフォーマンス問題完全無視

      • コード全体に、看過できない非効率が大量に広がる
      • いとも簡単に、要件よりも5-10倍も遅くなってしまう
      • チリツモなので後から改善するのが困難
  • ベストはこの間のどこか

    • 「自然に効率的」で、かつ綺麗で単純な選択を
  • 本質的に高コストな処理を押さえて開発するのが鍵

    • ネットワーク通信
    • 副記憶装置のI/O
    • 動的メモリ割り当て
    • キャッシュミス
  • 何が高コストか知るには、ベンチマークをとるのが最良

    • ベンチマークフレームワークは数日程度で作れる
    • RAMCloudの開発時にお世話になった
  • 処理のコスト感を掴んだら、それを元に低コストな処理を選ぶ
  • 多くの場合、同程度に単純だが、より効率的な処理というものが存在する

    • 例: Hash vs OrderedMap

      • 【補】ハッシュテーブルはO(1)
      • 【補】順序木はO(log(n))
    • 例: メモリ割り当て

      • 構造体のポインタの配列

        • 割り当てが複数回必要

          1. ポインタの配列の割り当て
          2. 個々の構造体の割り当て
      • 構造体の配列

        • 一撃で割り当てできる
        • 【補】ポインタ分の空間も節約できる
        • 【補】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

    • 無関係な、異物の