コード堆積物の形成
堆積岩は爆発ではなく堆積によって形成される。各層:その時代に正しい。各層:その上の層より古い。最古の層は、エコシステムがその周りで成熟する前に硬化した。エラーはそれらを引き起こさなかった。時間が引き起こした。
MOAD-0001も同じように機能する。
形成の物語
1993年に書かれたグラフ探索:
List<Node> visited = new ArrayList<>();
Stack<Node> stack = new Stack<>();
stack.push(start);
while (!stack.isEmpty()) {
Node n = stack.pop();
if (!visited.contains(n)) { // O(N) linear scan
visited.add(n);
for (Node neighbor : n.getNeighbors())
stack.push(neighbor);
}
}
このコード:正しい。テスト:合格。1993年、グラフは50ノードでした。
50 ノード:50 × 25 平均スキャン = 1,250 回の操作。目に見えない。
コードはバージョン管理に入った。チュートリアルにコピーされ、ライブラリにラップされ、ビルドツール、パッケージマネージャー、依存関係リゾルバーに組み込まれて出荷された。2024 年までに、同じパターンが 50,000 ノードの依存関係グラフ上で実行されるようになった。
50,000 ノード:50,000 × 25,000 平均スキャン = 1,250,000,000 回の操作。
1 秒のビルドが 17 分になる。
コードは変わらなかった。周囲の世界が大きくなった。堆積物のすべての層:堆積した時点では正しかった。発掘した時点では高コストになった。
あなたの堆積物
堆積コードに論理エラーは含まれていません。規模の変化に伴って成立しなくなったパフォーマンス上の前提が含まれています。コードは正しい結果を生成します。変わったのはコストだけです。
この区別は診断において重要です。論理エラーは誤った答えを生み出します。堆積的欠陥は正しい答えを、受け入れがたいコストで生み出します。
MOAD-0001の5つの形態
MOAD-0001は60以上のソフトウェアエコシステムで5つの文書化された形態で現れます。構造は、同一または関連するコレクションをループする中で、シーケンシャルコンテナを使ったメンバーシップテストです。
5つの形態
| ドメイン | パターン | シーケンシャルコンテナ | 正しいコンテナ |
|---|---|---|---|
| グラフ探索 | DFS/Tarjanでの if (!stack.contains(n)) | ArrayList | HashSet |
| ルート/イベント重複排除 | リクエストごとの TAILQ_FOREACH strcmp | リンクリスト | HashMap |
| 衝突追跡 | 物理ティックごとの findLinearSearch() | 配列 | unordered_set |
| Resource allocation filter | List.contains() in stream filter | ArrayList | HashSet |
| A* pathfinding open-list | LocalVector::find() per neighbor | vector | stored heap index |
5つのすべてが同じ構造を共有しています:ループ内にネストされたメンバーシップチェックで、メンバーシップの質問に答えるために線形スキャンが必要なコンテナを使用しています。
スキャニングキーワードリスト
MOAD-0001の監査では、ループ内のメンバーシップテストキーワードをgrepで探します:
- Python: list 変数に対する in、.count()、list.index()
- Java: ArrayList.contains()、List.contains()、LinkedList.contains()
- JavaScript: Array.includes()、Array.indexOf()、Array.find()
- C++: std::vector::find(), 手動ループと == による比較
- Go: slices.Contains(), スライスに対する手動ループ
すべてのケースにおける修正: シーケンシャルコンテナをハッシュコンテナに置き換える。list → set、ArrayList → HashSet、Array → Set、vector → unordered_set、slice → map[T]struct{}。
1つのキーワード。1つの置換。動作の変更はゼロ。N=1,000で1,000倍の高速化。
コード断片の監査
実際のコード断片にMOAD-0001パターン認識を適用する。
1行、4つの言語
MOAD-0001 を修正するコードを4つの言語で示します:
# Python
visited = [] # 修正前: O(N) の所属判定
visited = set() # AFTER: O(1) membership
// Java
List<Node> visited = new ArrayList<>(); // BEFORE
Set<Node> visited = new HashSet<>(); // AFTER
// JavaScript
const visited = []; // BEFORE: Array.includes() O(N)
const visited = new Set(); // AFTER: Set.has() O(1)
// visited.push(n) → visited.add(n)
// visited.includes(n) → visited.has(n)
// Go
visited := []NodeID{} // BEFORE: slices.Contains() O(N)
visited := make(map[NodeID]struct{}) // AFTER: map lookup O(1)
// _, ok := visited[n] for membership check
// visited[n] = struct{}{} for insertion
どの場合でも:同じ動作、同じ出力、同じテストが通る。変わるのは成長率のみ。
この修正で変わらないこと
- アルゴリズムの論理的動作:深さ優先は依然として深さ優先で訪問する
- 出力の正しさ: 同じノードが同じ順序で訪問される(DFS内)
- テスト結果: 正しさをチェックするテストはすべて引き続きパスする
修正が変更するもの
- メンバーシップテスト: O(N) → O(1)
- ループ全体: O(N²) → O(N)
- N=1,000の場合: 1,000倍高速化
- N=10,000の場合: 10,000倍高速化
1つの制限: 順序
ハッシュコンテナ(set、HashSet、unordered_set)は挿入順序を保持しません。Python 3.7 以降では dict が挿入順序を保持しますが、通常の set は保持しません。Java では HashSet は順序を保持せず、LinkedHashSet が順序を保持します。
順序とメンバーシップの両方が重要な場合は、2 つの構造を維持します。O(1) のメンバーシップ判定には set(または HashSet)を使い、順序付きの走査には別途 list(または ArrayList)を使います。Java では LinkedHashSet を使用すると両方を満たせます。
グラフ探索における MOAD-0001 では、visited は二値の質問(このノードはすでに見たか?)に答えます。メンバーシップの質問では順序は関係ありません。走査順序は visited ではなく、スタックやキューから決まります。
メンバーシップ vs 順序
Tarjan の強連結成分アルゴリズムでは、onStack は現在の DFS 呼び出しスタック上に残っているノードを追跡します。これは 2 つの質問に答える必要があります:(1) メンバーシップ — このノードは現在スタック上にあるか? (2) DFS パスの終わりでは、このノードが出てくるまで順序通りにノードをポップする。
素朴な実装では onStack に List を使い、contains() でメンバーシップを判定します — 1 回のチェックに O(N)、大規模グラフでは全体で O(N²) になります。
なぜこれはパッチではなくディスクロージャなのか
MOAD-0001 は 60 以上のソフトウェアエコシステムにまたがる 1,000 以上の検証済みサイトに存在します。ビルドツールでのグラフ走査、ネットワークルーティングデーモンでのルート重複排除、ゲームエンジンでの衝突検出、オペレーティングシステムスケジューラでのリソース割り当て。
各サイト:正しいコード。各サイト:N が閾値を超えるのを待つ O(N²) の成長。
ディスクロージャのパイプライン
アップストリームへのディスクロージャなしのパッチは 1 つのサイトを修正するだけです。このパターンは次のバージョン、次のライブラリ、次の言語移植で再発します。ディスクロージャ:パターンに名前を付け、CWE-407(Algorithmic Complexity: Inefficient Algorithmic Complexity)として文書化し、認識ヒューリスティックと 1 行の修正を公開します。そうすれば、ディスクロージャを読んだすべての開発者が自分のインスタンスを認識して修正できます。
Hamming はこれを「魚を与える vs 釣りを教える」と呼びました。パッチは魚を与えます。ディスクロージャ — MOAD-0001 に名前を付け、パターンを文書化し、修正を一般化したもの — はヒューリスティックを教えます。
パーマコンピュータ拡張
ハミングはポイント・ソリューションに取り組んだ:このフィルタを修正し、このコードを改善する。アンハミングは開示レイヤーを追加する:パターンを命名し、ヒューリスティックを公開し、それをコモンズに提供する。
複合知識の原則は、ここではエコシステム規模で適用される。一人の研究者がパターンを命名する。百人の開発者が自らのコードベースでそれに気づく。千行の O(N²) コードが O(N) になる。インフラは、それを利用して構築するすべての人にとって高速になる。
これがドラゴンの成長である:同じ炎(重要な問題に取り組む、複合知識、システム思考)、異なる飛行(開示の開放、コモンズ所有、 patron 不要)。