2 ポイント 投稿者 GN⁺ 2023-11-02 | 1件のコメント | WhatsAppで共有
  • 1989年のRob Pikeによる、プログラミングの5つの規則に関する記事
  • 規則1: プログラムがほとんどの時間をどこで費やすかを決めつけてはならない。ボトルネックは予想外の場所で発生しうる。ボトルネックが実証されるまで速度ハックは避けよ。
  • 規則2: 速度のためにチューニングする前に、必ず測定せよ。コードの一部が残りに大きな影響を与える場合にのみ最適化せよ。
  • 規則3: n が小さいとき、複雑なアルゴリズムは遅い。たいていの場合はこれに当てはまる。n がしばしば大きい場合にのみ複雑なアルゴリズムを使い、その場合でもまず規則2を適用せよ。
  • 規則4: 単純なアルゴリズムとデータ構造が望ましい。複雑なものよりバグに弱くなく、実装もしやすい。
  • 規則5: 正しいデータ構造はプログラミングにおいて決定的である。データが適切に構成されていれば、アルゴリズムは自明になる。
  • Pikeの規則1と2は、Tony Hoareの格言「早すぎる最適化は諸悪の根源である」を反映している。
  • Ken Thompsonは、Pikeの規則3と4を「疑わしいときは brute force を使え」と言い換えた。
  • 規則3と4は、KISS(Keep It Simple, Stupid)という設計哲学を体現している。
  • 規則5は、Fred Brooksの『The Mythical Man-Month』での発言と一致しており、しばしば「賢いオブジェクトを使う愚かなコードを書け」と要約される。

1件のコメント

 
GN⁺ 2023-11-02
Hacker News のコメント
  • データが支配する」という言葉には完全に同意する
    だから LeetCode の面接はいつも妙に感じていた。たいていアルゴリズムに焦点を当てるが、実際には最初からそうアプローチすべきでない場合が多く、データ構造のほうが中心であるべきだ
    もちろん、アルゴリズムをまったく知らないと、例外的な状況や特定の理由で特定のアルゴリズムに頼るべき場面に気づけないことはある。それでもアルゴリズムは比較的短く教えられる一方で、どのデータ構造を使うべきかの勘所をつかむほうが、人はより苦手なように思う

    • 自分の経験でも同意する。面接で FizzBuzz のようなアルゴリズム確認を超えて、すぐにデータ構造、アーキテクチャ、ドメインとの対応を話し始めると、面接官がはるかに敬意を示すのを見てきた
      その瞬間、「あ、本当にシニアエンジニアが来たな」という空気に変わり、技術的な問題をもっと開かれた形で話すようになって、「ちゃんとコーディングできるか」を証明させようとする態度も薄れる
      逆に、良い変化を作ること、マイルストーンの達成、チーム協業で最も苦労したチームは、データ構造とコードアーキテクチャをきちんと作れる人がいない場所だった。フレームワークが全部やってくれて、だめならもっと賢い誰かが作ったプラグインやミドルウェアが解決してくれる、という考え方に慣れた人が増えたように思う
      データ構造を避けるエンジニアは自分の足を撃っているようなもので、最も有用な道具の一つを捨てているのだから、日常的に限界が露呈する
    • 競技プログラミング大会の準備をしているおいを手伝ってみると、ほとんどの問題ではデータを適切なデータ構造に変換することが解決の大きな部分だった
      たとえばコードは重み付き有向非巡回グラフ(DAG)で最長経路を見つけるのに多く使えるが、核心は問題を重み付き DAGとして表現できると気づくことにあった。それが見えなければ解けるとしても、はるかに遅く複雑な解法になる
    • 一般的な LeetCode の問題も実際にはデータ構造に焦点を当てている。候補者が問題と解法をパターンマッチングするとき、頭の中から取り出すためのデータ構造の一覧を持っている必要があるからだ
      面接官が優先度付きキュー、隣接行列、トライのようなものを使うべきだと先に教えてくれるわけではない。行き詰まればヒントはもらえるかもしれないが、過度な誘導は強い採用シグナルとは見なしにくい
    • 「フローチャートは見せても表を隠せば、私はずっと混乱したままだろう。表を見せてくれれば、フローチャートを見る必要はない。自然と明確になるのだから」
    • どちらか一方を選ぶには、もう一方についての大まかな感覚も必要ではないかと思う。データにどうアクセスするかをまったく知らなければ、どのデータ構造を使うべきか分かりにくい
  • 「すばらしいアルゴリズムは n が小さいと遅く、そして n はたいてい小さい」という言葉に関連して、最近のプロジェクトで実感したのは、大きな n は思ったよりずっと大きくなり得るということだ
    「10万回演算するのだから必ず最適化しなければ」と考えがちだが、コンピュータは速く、10万回の乗算程度ならたいていあまりに速いので深く悩む必要がないかもしれない
    何も考えるなと言いたいわけではないが、現代のハードウェアがどれほど狂ったように速いかには、しばしば驚かされる

    • この意見には強く同意しづらい。二次時間アルゴリズムは予想外の瞬間に食らいやすい類いだからだ
      うっかり二次時間になったコードが本番障害を起こしたのを見たことがあるし、99% のユーザーがいつも小さな n を使うとしても、一部のユーザーはしばしば大きな n に出会って非常に遅いアプリを体験することがある
      ほとんどの場合、通常ケースで少し遅く実装が少し複雑になるとしても、二次時間より良いアルゴリズムを選びたい。よくある遅い経路は最適化されるが、まれな遅い経路は開発者自身が踏まずに見逃したり、本番で爆発したりする
      もちろん、あまりに複雑なアルゴリズムなら単純な二次時間実装を選ぶこともあるが、デフォルトではできるだけ二次未満にしたい。関連して書いた記事もある: https://kevincox.ca/2023/05/09/less-than-quadratic/
    • メモリ階層もこれに影響する。多くのすばらしいアルゴリズムは参照局所性が悪く、分岐も増える
      そのため、CPU がメモリに対してそこまで速くなく、コンシューマ向けハードウェアで分岐予測ミスをそれほど気にしていなかった 40 年前には、もっと当てはまっていたのかもしれない
    • LeetCode の面接問題で、10万件のリストを何度か走査するのをよく見る。最適ではないかもしれないが、実際の実行時間で見れば、その直後に行うネットワーク呼び出しに比べれば、10万件の走査など大したことではない
      面接のたびに、採用マネージャーは欲しがっているのに、本番の傷をまだ味わっていない LeetCode 初心者がその判断を拒むことが起きる
    • この話題の代表的な参考資料は Scalability! But at what COST?
      https://www.frankmcsherry.org/assets/COST.pdf
    • 2000年代初頭にゲーム会社で最初のまともなプログラミングの仕事を始めたとき、技術ディレクターが「扱う項目数が 1 万程度なら最適化するな」と助言してくれた
      この 20 年間のコンピュータ性能向上を考えると、その基準を10万に引き上げるのはかなり妥当に思える
  • 「早すぎる最適化は諸悪の根源」という有名な格言は Tony Hoare ではなく、実際には Donald Knuth に由来する言葉であり、最適化一般に反対する言葉として文脈なしに使われることが多い
    全文は「小さな効率、たとえば 97% のケースについては忘れるべきである。早すぎる最適化は諸悪の根源である。しかし重要な 3% においては、その機会を逃してはならない」というもの
    要点は、影響のある場所に時間を使って最適化せよ、ということ

    • Knuth は Hoare の言葉だと言い、Hoare は Knuth の言葉だと言うので、誰を信じるかの問題になる
      両方のものとして扱うのがいちばんよさそうだ。おそらく Tony が先に言い、Knuth が整えて出版した可能性がある。必要な文脈を与える長い引用を併記するのは、常によいことだ
    • この引用が 1970年代 末のものだという点も、しばしば忘れられる
      ほぼ 50 年前の話だ。当時のプログラミングは今とはかなり違っていた。そのころの「早すぎる最適化」は、「スケーラビリティの高い人気ライブラリをそのまま使おう」ではなく、「このハードウェアでしか動かない理解不能なビット操作アルゴリズムを使おう」に近かった
    • 長い引用が意味のある追加文脈を与えるとは思わない。重要な 3% を測定して見つけ出したのなら、その段階はもはや早すぎるわけではない
      それは「早すぎる最適化は諸悪の根源」という言葉にすでに含まれている意味であり、格言は「最適化は諸悪の根源」ではない
    • この言葉を教義のように受け取って、効率的な方法 そのものを学ばない人があまりにも多い
      会社のデータ構造・アルゴリズム面接で、バブルソートが最善だと言うフロントエンド開発者を数えきれないほど見てきた。その場で導出までできる必要はなく、いくつか知っていて、問題に合ったよい選択を言えれば十分だ
      「早すぎる最適化をするな」を極端に受け取りすぎて、効率的な方法すら知らないのなら、どこが重要かをどうやって見極めるのか
    • この文脈では、最適化一般に反対する意味で使われているわけではなさそうだ
  • 「データ構造が核心」という話は、データベース では二倍重要になる
    DB を愚かなビットの保存箱、あるいはオブジェクト定義の 1:1 の写し程度にしか使わない人は、DB がそれを個人的に受け取ったかのように性能を台無しにしてくると驚きがちだ
    ORM が生成した DB スキーマをまた見ることになったら、あまりに早すぎる再会だと思う

    • ほとんどの ORM は、要求されたとおりにスキーマを生成していると考えている。ORM を使うからといって、手で作るより悪い データベース設計 が自動的に生まれるわけではない
      問題は、一部または多くの開発者が SQL を知らず、ORM を使うのに必要な DB 知識も持っていないことにある
      ORM は、その下に何があるかを理解している必要がある、かなり漏れやすい抽象化だ。それを理解していれば、ほとんどの ORM でもまともなスキーマは作れる
    • ここには Conway の法則 も加えられる。「システムを設計する組織は、その組織のコミュニケーション構造を複製した設計を生み出す」というものだ
      データ構造をうまく整理し、設計が変わってもそうした状態を維持するには、データとコードを組織レベルで分離しなければならない
      DB スキーマ設計、ユースケース、その間のマッピングを他の実装から切り離し、このグループが整合性検証なども書くべきだ。組織構造がデータとコードを分離していなければ、コードとデータを分離するのは難しい
    • ストアドプロシージャが勝つ
  • 私の追加ルールは、小さな 性能の無駄 でも積み重なれば、それぞれは大したことがなくても最終的にはプログラムを遅くする、というものだ
    複雑さ、可読性、保守性、実装コストに影響がないなら、性能をただ捨て置くべきではない。他の条件がほぼ同じなら、二つの選択肢のうち遅い方を選ぶのはよくない
    また、n が小さいと仮定すれば、ほとんど何でも通用する。しかし n が 100 以下ではうまく動き、10000 以上で破綻するコード、たとえば O(n²) のようなものを使うなら、単に制限を設けるべきだ。小さな n という仮定が崩れたとき、大きなエラーを出して止まる方が、AWS の請求爆弾や停止したプログラムよりましだ

    • ここにはルール 1 と 2 が適用される
  • これらの指針のかなり多くは、結局 過剰設計 を防ぐ戦略に行き着く
    私の経験では、早すぎる最適化は最も高くつく罠の一つだ。潜在的な問題をあまりに早く回避しようとすると、その前提は検証されないままで、次のチームが不要な複雑さを解消するために高価な解決策を作る羽目になる
    学んだアプローチはこうだ。最適化は推測に頼り、初期の推測はしばしば間違う
    また、人が過度に複雑なコードを書かないようにするには、自尊心のマネジメント と心理への理解がかなり重要だということも分かった

    • 「いま持っている問題を解け。持っていると思っている問題を解くな」と言うことがある
    • この概念は、リーンやシックスシグマの 無駄の特定 にも通じる
      過剰生産は通常、最悪の無駄と見なされる。不要なものを作るだけでなく、本当に必要なことに使えたはずの労力まで使ってしまうからだ。過剰設計も同じようなものだ
    • さらに一段踏み込むと、過剰設計は、後で複雑性が必要になるかもしれず、そのときにシステムを拡張する方がより難しい、あるいは危険だと考えることで生じる
      たとえばユーザーが 100 人しかいないのに、いつか 100 万人になったときモノリスを再設計するのは難しいだろうとして、マイクロサービスアーキテクチャ で始めるようなものだ
      だからこそ、時間がたつにつれてコードがなぜ柔軟性を失っていくのかから先に対処すべきだ
    • エラー処理では気取らず、早い段階で単純に失敗 する方がよい
  • おおむね良いルールだが、実際には1番はそのままでは成り立たない。
    始める時点では、何がボトルネックになるかについての仮説が必要だ。XYZを実装してから何が遅いかを計測して直せばよい、というやり方が常に可能とは限らない。X、Y、Zが結び付いていて、Yを速くするためにXとZを特定の方法で作らなければならないこともあるし、Yがボトルネックになると最初から分かっている場合もある。
    後で計測して何が遅いかが分かったとしても、より高速にするためのアプローチに賭ける必要はある。より根拠のある賭けであるほどよい。
    良いプログラマは計測するが、何が遅く、何がバグが多く、何がメモリを多く使うかを予測できるので、反復が少なくて済む。性能の振る舞いは予測できない、とルールのように語るのは、優れたプログラマが積み上げてきた経験と技術を無視することになる。

    • 1番のルールは、信じない人にとっては鉄則で、信じる人にとっては緩い指針である。
      1番を守る過程こそが、ボトルネックを予想するための良い直感に必要な経験と経験的背景を得る最良の方法だからだ。
    • 遅そうなアルゴリズムは、スパイク実装で確認できる。たいてい遅いアルゴリズムは実装とテストが簡単だ。
      速度の予測が外れると、プロジェクトの寿命のあいだずっと不要に複雑なコードを抱えることになる。
      人はアルゴリズムの速度をしばしば見誤る。コンピュータが時間の99%をDBサーバーからnを取得するのに使っているなら、O(n)とO(n²)が実時間では同じに見えることも多い。
      Cで書いたアルゴリズムが、等価なPythonコードより遅いことすらあるが、それはバイトコードコンパイラが賢い最適化をしているからかもしれない。
      レガシーコードを高速化する仕事を数多くしてきたが、たいていは思っているよりずっと簡単で、しかも元の作者には明白でなかった理由で遅くなっている。実際には、コードベースが複雑になりすぎて、元の作者がもはや推論できなくなっているために遅いことが多い。私にとっては「遅すぎる」という具体例があるので、実行しながら遅い箇所を観察してデバッグしやすい。
    • これは原文全体への反応ではないように思う。原文では、ボトルネックが分かるまで速度ハックを入れるなと言っている。説明されている状況とは違う。
      物理オブジェクトが大量にあるビデオゲームを作っていて、経験上、衝突検知が大きな問題になると確実に分かっているなら、ゲームとシステムをそれ中心に設計するのは速度ハックではない。
      性能が大きな関心事になると分かっている作業なら、当然計測すべきだ。関心事かどうかを確かめるためではなく、その関心事をどれだけうまく扱えているかを確かめるためである。
    • 具体例が気になる。ほとんどの場合、本当に差が出るのか疑わしい。
      新しい要求に対して新しいシステムを作るなら、まず始めてしまってよいことが多いのではないかと思う。作って、テスト・計測して、捨てるかリファクタリングして、また繰り返せばよい。
      Rustを例にすると、試作言語とOCaml製のコンパイラから始めて反復してきた。いつかOCamlからセルフホスティングへ移れると分かっていたとしても、それが大きな違いを生んだかはよく分からない。
    • 開発者が何が遅くなるかをそこまで正確に予測できるなら、開発者主導スタートアップの成功率は100%であるべきではないかと思う。
      ユーザーがいなければ、数時間かかる関数でも、最適化すればミリ秒になる関数と比べてなお十分に速い。そういう予測を正確にできることを立証した人がいるのかは、あまり分からない。
  • 5番への反論として、単純なデータの上での複雑なアルゴリズムが、大きな性能向上をもたらし、障害を取り除き、かえって単純化につながることもある。
    たとえばBinaryTreeオブジェクトの代わりに、ソート済み配列に対する二分探索を使えば、マージはconcatしてからソートするだけなので単純になり、ポインタがないのでシリアライズが容易で、場合によってはシリアライズ自体が不要になる。配列はディスク上にもメモリ上にも、あるいはmmapでその両方にも置けるし、RAMより大きいデータも扱える。ファイルやマッピングを指すだけですぐ動かすコールドスタートも可能だ。キャッシュ無視(cache-oblivious)の性質もある。
    Huffman符号化も例の一つだ。大学ではたいてい木ベースのアルゴリズムとO(n log n)の計算量で学んだが、インプレースな配列ベースでHuffman木を線形時間に構築する方法があることは知らなかった。
    もちろん、時間の99%は標準的なコレクションデータ構造を使ってバックエンドのマイクロサービスを作っている。しかし仕事でビッグデータを扱うなら、その時々の流行であるMapReduce系を導入するより、ローカルに大容量ディスクを積んだ単一マシンで処理するほうをはるかに好む。

    • 二分探索をすごいアルゴリズムだとは思わない。現代のソート関数のほうがすごい側で、微妙なバグが入り得るので一般の開発者が自作すべきではない。クイックソートですら落とし穴がある。
      Rob Pikeなら、まずコードをプロファイリングし、そのうえで気の利いたコードや代替データ構造が本当に速いかどうかを見ろと言っただろう。
    • これは反論には見えない。Pikeの助言の観点では、「ソート済み配列に対する二分探索」と「BinaryTreeオブジェクト」は、同じデータ構造の異なる実装にすぎない。
    • 開発者が99%の時間において最も高価な資源であることを忘れてはならない。保守性と市場投入までの速さのほうが、たいていはるかに重要だ。
  • この記事は10年以上前にcat-vで初めて読んだが、設計や複雑さへの向き合い方、考え方に消えない影響を受けた。
    http://doc.cat-v.org/bell_labs/pikestyle

  • 元のルールは「データ構造が肝心だ」なのに、どうして「賢いオブジェクトを使う愚かなコードを書け」に要約されるのか分からない。
    “smart objects”という表現はいまひとつで、元のルールは長くてもずっと良い。

    • Rob Pikeも“smart objects”は間違った考え方だと同意しそうだ: https://commandcenter.blogspot.com/2012/06/less-is-exponenti...
    • 「スマート」なロジックは、より高いレベルへ引き上げたほうが、理解しやすく、テストしやすく、変更もしやすい。smart objectsは、互いに凝集性を持たせて作るのがはるかに難しいと思う。
    • よく構造化されたオブジェクトから自然に導かれるコードを書け、という意味だと考えればよい。