English· Español· Deutsch· Nederlands· Français· 日本語· ქართული· 繁體中文· 简体中文· Português· Русский· العربية· हिन्दी· Italiano· 한국어· Polski· Svenska· Türkçe· Українська· Tiếng Việt· Bahasa Indonesia

un

guest
1 / ?
back to lessons

程式碼沉積物如何形成

沉積岩是由堆積而非爆炸形成。每層:在其時代是正確的。每層:比上面的層更古老。最古老的層在生態系成熟前就已硬化。它們不是由錯誤造成的,而是由時間造成的。

MOAD-0001 的形成方式相同。

MOAD-0001 沉積層:隨著 N 增長,程式碼跨數十年被複製

形成故事

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))ArrayListHashSet
路由/事件去重每次請求使用 TAILQ_FOREACH 進行 strcmplinked listHashMap
碰撞追蹤每次物理刻使用 findLinearSearch()arrayunordered_set
資源分配過濾器List.contains() 於串流過濾器ArrayListHashSet
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 進行迴圈搜尋

在所有情況下的修正方式:將循序容器替換為雜湊容器。listsetArrayListHashSetArraySetvectorunordered_setslicemap[T]struct{}

一個關鍵字、一次替換、零行為改變。在 N=1,000 時可獲得 1,000 倍的速度提升。

審查程式碼片段

將 MOAD-0001 模式辨識套用至真實的程式碼片段。

你審查一個 JavaScript 程式碼庫,發現以下程式碼位於遍歷所有圖形鄰居的迴圈內:`if (!openSet.includes(neighbor)) openSet.push(neighbor)`。這是否屬於 MOAD-0001?應該將 `openSet` 替換為什麼?複雜度會從 O(?) 變成 O(?)?

一行程式碼,四種語言 [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²)。

你將 Tarjan SCC 實作中的 `onStack = []` 改成 `onStack = set()`,測試通過。程式碼審查者問:「但如果 onStack 需要順序呢?」你該如何回答?

為什麼這是揭露而非修補

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]

這就是龍的成長:相同的火焰(處理重要問題、複合知識、系統思考),不同的飛行(開放揭露、公共所有權、無需贊助者)。