程式碼沉積物如何形成
沉積岩是由堆積而非爆炸形成。每層:在其時代是正確的。每層:比上面的層更古老。最古老的層在生態系成熟前就已硬化。它們不是由錯誤造成的,而是由時間造成的。
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) 線性掃描
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 的五種形式
MOAD-0001 在 60 多個軟體生態系統中以五種已記錄的形式出現。其結構為:在對同一或相關集合進行迴圈的過程中,使用循序容器執行成員測試。
五種形式
| 領域 | 模式 | 循序容器 | 正確容器 |
|---|---|---|---|
| 圖形遍歷 | DFS/Tarjan 中的 if (!stack.contains(n)) | ArrayList | HashSet |
| 路由/事件去重 | 每次請求使用 TAILQ_FOREACH 進行 strcmp | linked list | HashMap |
| 碰撞追蹤 | 每次物理刻使用 findLinearSearch() | array | unordered_set |
| 資源分配過濾器 | List.contains() 於串流過濾器 | ArrayList | HashSet |
| A* 路徑搜尋開放列表 | 每個鄰居呼叫 LocalVector::find() | vector | 已儲存的堆積索引 |
所有五種形式都擁有相同的結構:在迴圈中進行成員檢查,並使用需要線性掃描才能回答成員問題的容器。
掃描關鍵字清單
對 MOAD-0001 進行稽核,意味著在迴圈內搜尋成員測試關鍵字:
- Python:in 搭配 list 變數、.count()、list.index()
- Java:ArrayList.contains()、List.contains()、LinkedList.contains()
- JavaScript:Array.includes()、Array.indexOf()、Array.find()
- C++: std::vector::find(), 手動使用 == 比較的迴圈
- Go: slices.Contains(), 手動對 slice 進行迴圈搜尋
在所有情況下的修正方式:將循序容器替換為雜湊容器。list → set、ArrayList → HashSet、Array → Set、vector → unordered_set、slice → map[T]struct{}。
一個關鍵字、一次替換、零行為改變。在 N=1,000 時可獲得 1,000 倍的速度提升。
審查程式碼片段
將 MOAD-0001 模式辨識套用至真實的程式碼片段。
一行程式碼,四種語言 [BLOCK_TYPE CONTENT fix_and_limits/the_fix]
修正 MOAD-0001 的四種語言版本: [BLOCK_TYPE CONTENT fix_and_limits/the_fix]
```python [BLOCK_TYPE CONTENT fix_and_limits/the_fix]
Python
[BLOCK_TYPE CONTENT fix_and_limits/the_fix]visited = [] # BEFORE: O(N) membership
visited = set() # AFTER: O(1) membership
```java
// Java
List<Node> visited = new ArrayList<>(); // BEFORE
Set<Node> visited = new HashSet<>(); // AFTER
// JavaScript
const visited = []; // 之前:Array.includes() O(N)
const visited = new Set(); // 之後: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 倍
一個限制:順序
雜湊容器(set、HashSet、unordered_set)不會保留插入順序。在 Python 3.7+ 中,dict 會保留插入順序,但一般的 set 不會。在 Java 中,HashSet 不保留順序;LinkedHashSet 則會。
如果同時需要順序與成員資格:請維護兩個資料結構。用 set(或 HashSet)負責 O(1) 的成員資格檢查;另外用 list(或 ArrayList)負責有序遍歷。或者在 Java 中使用 LinkedHashSet,它同時提供兩種功能。
在圖形遍歷的 MOAD-0001 中:visited 只回答一個二元問題(此節點是否已被看過?)。成員資格問題本身不涉及順序。遍歷順序是由 stack 或 queue 決定,而非由 visited 決定。
成員資格 vs 順序
在 Tarjan 的強連通分量演算法中,onStack 追蹤哪些節點仍在目前的 DFS 呼叫堆疊上。它必須回答兩個問題:(1) 成員資格 — 此節點目前是否在堆疊上?(2) 在 DFS 路徑結束時,依序彈出節點直到遇到此節點。
一個天真的實作會使用 List 來存放 onStack,並以 contains() 回答成員資格問題 — 每次檢查為 O(N),大型圖形總時間複雜度為 O(N²)。
為什麼這是揭露而非修補
MOAD-0001 存在於 60 多個軟體生態系、超過 1,000 個已驗證的站點中。包括建置工具中的圖形遍歷、網路路由守護程序中的路由去重、遊戲引擎中的碰撞偵測,以及作業系統排程器中的資源分配。
每個站點:程式碼正確。每個站點:等待 N 超過門檻時,O(N²) 的成長就會發生。
揭露流程
沒有上游揭露的修補只能修復單一站點。該模式會在下一個版本、下一個函式庫、下一個語言移植中再次出現。揭露:命名該模式,將其記錄為 CWE-407(演算法複雜度:低效率演算法複雜度),並公開辨識啟發式與單行修正。如此一來,每位閱讀此揭露的開發者都能辨識並修復自己的實例。
Hamming 稱此為「給魚 vs 教人釣魚」。修補是給魚。揭露——命名 MOAD-0001、記錄模式、推廣修正——則是教導啟發式。
永續電腦延伸
[BLOCK_TYPE SECTION/STEP]漢明專注於單點解決方案:修復這個過濾器、改善這段程式碼。非漢明則加入揭露層:命名模式、公開啟發法,並將其貢獻給公共領域。 [BLOCK_TYPE SECTION/STEP]
複合知識原則在此以生態系統規模發揮作用。一位研究者命名一個模式。一百位開發者在其自己的程式碼庫中辨識出它。一千行 O(N²) 程式碼變成 O(N)。基礎設施對所有在其上建構的人來說都變得更快。 [BLOCK_TYPE SECTION/STEP]
這就是龍的成長:相同的火焰(處理重要問題、複合知識、系統思考),不同的飛行(開放揭露、公共所有權、無需贊助者)。