未来を形作るテクノロジーの深掘り記事。

Rob Pikeのプログラミング5原則は今も通用する

Rob Pikeが1989年に書いたプログラミングの5つのルール。今こそ重要性が増しており、特に私たちが無視しがちなルールを再確認します。

一点照明に照らされた散らかった現代的なデスクの横にピン留めされた古いインデックスカード

1989年、後にGo、UTF-8、Plan 9を共同開発することになるRob Pikeが、プログラミングの5つのルールを書き残しました。インデックスカード一枚に収まるほど短い一方で、その奥深さのおかげで、プログラミング界は37年間もこれについて議論を続けています。多くの開発者がブログ記事やカンファレンスの講演で、そのうちのいくつかを目にしたことがあるでしょう。ただ、きちんと腹落ちさせている人はそれほど多くありません。もったいない話です。というのも、これらを身につければ無駄な労力をかなり減らせるからです。

このルールは一見シンプルです。構文やアーキテクチャのパターンについて書かれたものではありません。プログラマーがどこで決まって時間を無駄にするのか、そしてその無駄をどう止めるのかについて書かれたものです。

5つのルール

細かく見る前に、まずは原文どおりに列挙しておきましょう。

  1. プログラムがどこで時間を使うかは予測できません。ボトルネックは意外な場所に潜んでいるので、そこだと証明できるまでは、先回りして速度改善のハックを入れてはいけません。
  2. 測定せよ。測定するまでは速度のチューニングをしてはいけません。さらに、コードの一部が他を圧倒している場合を除いては、測定したあとでもチューニングしてはいけません。
  3. 凝ったアルゴリズムは、nが小さいときには遅くなります。そしてnはたいてい小さいものです。凝ったアルゴリズムは定数項が大きいのです。nが頻繁に大きくなると分かるまでは、凝ったことをしてはいけません。
  4. 凝ったアルゴリズムは、単純なものよりもバグが多く、実装もずっと難しくなります。シンプルなアルゴリズムと、シンプルなデータ構造を使いましょう。
  5. データがすべてを支配します。適切なデータ構造を選び、物事をうまく整理できていれば、アルゴリズムはほぼ自明になります。プログラミングの中心にあるのはアルゴリズムではなく、データ構造です。

ルール1と2は最適化に関するもの、ルール3と4は複雑さに関するもの、ルール5は設計に関するものです。全体としては、謙虚さについての哲学だと言えます。つまり、パフォーマンスに関する自分の直感は間違っていること、複雑さのコストは過小評価されがちであること、そしてどれほど賢いコードよりも良いデータ構造のほうが重要であることを認めることです。

ルール1: ボトルネックがどこか、あなたは分かっていない

開発者が最も自信満々に破ってしまうのがこのルールです。「この関数はネストしたループがあるから遅いに決まっている」「検索がO(1)だからハッシュマップを使おう」「割り当てが高コストだから配列は事前に確保しておこう」。どれももっともらしく聞こえます。でも、たいていは間違っています。

私はこれまで十分な数の本番システムをプロファイリングしてきたので、直感で見当をつけたボトルネックが実際のボトルネックではなかった例をたくさん持っています。全員がデータベースがボトルネックだと思い込んでいたシステムでは、プロファイリングの結果、JSONのシリアライズがリクエスト時間の60%を消費していました。「高コストな」行列演算が実行時間のわずか5%で、CSVのパースが70%を占めていたデータパイプラインもあります。チームが何か月もかけてデータベースクエリを最適化していたウェブアプリケーションでは、実際のボトルネックは、送信HTTPリクエストごとに発生するDNS解決でした。

人間の脳は、プロファイラーとしては最悪です。概念的に高コストに見える処理(データベースクエリやネットワーク呼び出し)は過大評価し、安く見える処理(文字列連結、JSONパース、メモリ割り当て)は過小評価してしまいます。現代のハードウェアはこれをさらに悪化させています。CPUキャッシュ、分岐予測、アウトオブオーダー実行があるため、コードの複雑さと実行時間の関係は、直感からは大きく外れるのです。

ルール2: 測定してから最適化する

これはルール1の実践的な帰結です。直感に基づいて最適化してはいけません。まずプロファイルを取り、実際のホットスポットを見つけ、そこだけを最適化しましょう。

このルールの後半、「コードの一部が他を圧倒している場合を除いては」という部分も同じくらい重要ですが、引用されることは少ないです。プロファイラーの結果、実行時間が20個の関数に5%ずつ均等に分散していたとします。その場合、最適化すべき単一のボトルネックは存在しません。ある関数を2倍速くしても、全体の実行時間は2.5%しか減りません。それに見合うだけの複雑さを追加する価値はほぼありません。個々の関数を最適化するのではなく、根本的に異なるアプローチを探す必要があります。

# Profile first. Always.
# Python
python -m cProfile -s cumulative my_script.py
# Node.js
node --prof app.js
node --prof-process isolate-*.log > profile.txt
# Go
go test -bench=. -cpuprofile=cpu.prof
go tool pprof cpu.prof
# Rust
cargo flamegraph
# General (Linux)
perf record ./my_program && perf report
# The flamegraph (brendangregg.com/flamegraphs) is the most
# informative visualization. It shows you exactly where time
# is spent, in a format that makes bottlenecks visually obvious.

ルール3: 凝ったアルゴリズムは、Nが小さいと遅い

これは、コンピュータサイエンスの教育が逆になっている点です。私たちはO(n log n)がO(n²)より優れていると教えますが、これは漸近的には正しいです。しかしn=20の場合、実装のよいO(n²)の挿入ソートは、定数項、キャッシュの振る舞い、オーバーヘッドのおかげで、O(n log n)のマージソートより速いのです。

実世界でも例はたくさんあります。50要素のソート済み配列に対する線形探索は、二分探索より速いことがあります。線形探索はキャッシュの振る舞いが完璧で、分岐予測ミスもないからです。単純な連結リストは、要素数が約100未満の集合では平衡二分木より優れています。ツリーをポインタでたどるとキャッシュの局所性が壊れるからです。ハッシュマップは償却O(1)で検索できますが、定数項が大きいため、約30〜50要素未満の集合では配列への線形探索のほうが速いことがあります。

標準ライブラリはこれを理解しています。PythonのsortedはTimsortを使い、小さな部分列では挿入ソートにフォールバックします。C++のstd::sortは、しきい値(通常16〜32要素)未満では挿入ソートに切り替わります。Rustのsort_unstableはクイックソートと挿入ソートを組み合わせています。「凝った」アルゴリズムは、nが本当に大きくなって勝てる場合にだけ使われるのです。

より広い教訓は、「nを知っておけ」ということです。単純なO(n²)のアルゴリズムと複雑なO(n log n)のアルゴリズムのどちらかを選ぶなら、実際にnがどのくらいの大きさになるかを自問してください。数百未満であれば、単純なアルゴリズムでほぼ間違いなく問題ありません。しかも、書くのも、デバッグするのも、保守するのも楽になります。

ルール4: シンプルは賢さに勝る

ルール4はルール3をパフォーマンス以外にも広げたものです。凝ったアルゴリズムは、小さいnで遅いだけでなく、バグも多いのです。赤黒木は、ソート済み配列よりもエッジケースが多いです。ロックフリーな並行データ構造は、ミューテックスで保護されたものよりも微妙な障害モードを持ちます。独自のメモリアロケータは、システムのアロケータよりもメモリを壊す方法が多いのです。

私は、O(1)操作を持つ独自のLRUキャッシュの実装とデバッグに何週間も費やしているチームを見てきました。一方、単純な上限付き配列と線形の追い出しで、半日で書けて、一発で正しく動き、ワークロードに十分な速さで済んだはずのケースです(そのワークロードのキャッシュエントリは、多くても数百でした)。

複雑さのコストは、最初の実装だけに現れるわけではありません。将来そのコードを理解し、修正し、デバッグする必要があるすべての開発者にかかってきます。チーム全員が理解できる単純なアルゴリズムは、元の作者にしか保守できない賢いアルゴリズムよりも価値があります。そして、元の作者も6か月後にはほぼ別人になっていて、どう動くのかを忘れてしまっているものです。

「デバッグはコードを書くことの2倍難しい。だから、可能な限り賢くコードを書いてしまったら、定義上、それをデバッグできるほど賢くはないということになる。」 — Brian Kernighan

ルール5: データがすべてを支配する

これはPikeの最も重要なルールであり、アルゴリズムやデザインパターンに注力する開発者に最も見落とされているルールです。主張はこうです。データ構造が正しければ、アルゴリズムは自然に従う。データ構造が間違っていれば、どれほどアルゴリズム的な賢さがあっても救えない。

Fred Brooksも似たことを言っています。「フローチャートを見せて表は隠すなら、私はいつまでも困惑したままだ。表を見せてくれれば、たいていフローチャートは要らない」。Linus Torvaldsも同じ考えを示しています。「悪いプログラマーはコードを心配する。良いプログラマーはデータ構造とその関係を心配する」。

この原則は実践でも頻繁に現れます。ユーザー権限を文字列の平坦なリストとして保存しているコードベースは、権限チェックのロジックが複雑でミスの起きやすいものになり、コード中に散らばっていきます。データをロールの階層として再構成すれば、チェックのロジックは自明になります。イベントをJSONのブロブとして保存しているシステムでは、コンシューマごとに複雑なパースとバリデーションが必要になります。イベントを明示的なスキーマを持つ型付きレコードとして構造化すれば、コンシューマは劇的にシンプルになります。

# Bad data structure → complex algorithm
class OrderSystem:
def __init__(self):
self.orders = []  # flat list of all orders
def get_user_orders(self, user_id):
return [o for o in self.orders if o.user_id == user_id]  # O(n)
def get_pending_orders(self):
return [o for o in self.orders if o.status == 'pending']  # O(n)
def get_user_pending_orders(self, user_id):
return [o for o in self.orders
if o.user_id == user_id and o.status == 'pending']  # O(n)
# Better data structure → algorithms become obvious
class OrderSystem:
def __init__(self):
self.orders_by_user = {}      # user_id → [orders]
self.orders_by_status = {}    # status → [orders]
def get_user_orders(self, user_id):
return self.orders_by_user.get(user_id, [])  # O(1)
def get_pending_orders(self):
return self.orders_by_status.get('pending', [])  # O(1)
# The right data structure makes the code self-evident.
# You don't need to think about algorithms at all.

Goがこれらのルールをどう体現しているか

Pikeのルールを見ると、Goの設計思想の原型が見えてきます。Pikeがこれらのルールを書いてから20年後に共同開発したGoは、賢さよりもシンプルさを体系的に優先する言語です。

  • ジェネリクスなし(当初) — シンプルなデータ構造を強制する。(ジェネリクスはGo 1.18で追加されましたが、それは十分にシンプルな設計が見つかるまで何年も反対し続けた後のことでした。)
  • 演算子オーバーロードなし — コードは見たままの意味になる。
  • 暗黙の型変換なし — 賢さよりも明示性。
  • 例外なし — エラーはその場で処理する。
  • 標準ライブラリのアルゴリズムは最小限 — 凝ったデータ構造ではなく、スライスとマップを使う。
  • 組み込みのプロファイリング(pprof) — 推測せず測定する。

Goはより表現力の豊かな言語を好む開発者から「退屈だ」と批判されることがよくあります。しかし、それこそがポイントです。Pikeのルールは、退屈なコードのためのレシピです。シンプルで、測定可能で、賢いアルゴリズムではなく良いデータ構造の上に作られたコード。Goは、そのレシピを言語にしたものだと言えます。

ルールが当てはまらない場合

どんなルールにも普遍性はなく、Pikeのルールにも正当な例外があります。パフォーマンスが重要なシステム(ゲームエンジン、データベースの内部、コンパイラ)では、nが本当に大きいため、凝ったアルゴリズムが必要な場合があります。毎秒何百万回も実行されるインフラのコードは、アプリケーションのコードでは正当化されない最適化を正当化します。そして、「単純な」アルゴリズムがO(n³)の計算量を持ち、控えめなnに対してさえ本当に許容できない場合もあります。

このルールはヒューリスティックであり、法則ではありません。その価値は、よくある偏りを正すことにあります。開発者は早すぎる最適化をしがちで、複雑すぎるアルゴリズムを使い、コードについて考えすぎてデータ構造について考えなさすぎるのです。Pikeのルールは、そうした傾向に歯止めをかけます。もし逆の偏りが当てはまる稀な状況、つまり本当により少なくではなくより多くの複雑さが必要な状況に陥ったなら、遠慮なく凝ってください。ただし、まずは測定しましょう。

Pikeがこれを書き残してから37年が経ちましたが、このルールは今でも、これまで公開されたプログラミングのアドバイスの中で最良のものの一つです。驚くような内容だからではありません。経験のある開発者の多くは、読めば「そりゃそうだ」と思うはずです。価値は、一貫して適用できるほど明確に書かれていることにあります。次に赤黒木や独自アロケータ、あるいはプロファイルもしていない「最適化」に手を伸ばしたときは、覚えておいてください。まず測定し、シンプルさを保ち、データ構造を正しく選ぶことです。