研究 Immutable.js 和其他高性能庫後的優化建議。
- List build (Transient): 4.8x slower than mutation
- List sequential sets: 19.4x slower than mutation
- Map build (Transient): 2.9x slower than mutation
- Map sequential sets: 5.0x slower than mutation
✅ Structural sharing (O(log n) path copying) ✅ Transient API with Edit tokens ✅ Tail buffer optimization (32 elements) ✅ Builder API (native array → persistent) ✅ 32-way branching (cache-friendly) ✅ Bitmap compression (HAMT)
發現他們的 editableVNode 函數:
function editableVNode(node, ownerID) {
if (ownerID && node && ownerID === node.ownerID) {
return node; // Already editable
}
return new VNode(node ? node.array.slice() : [], ownerID);
}結論: 與我們的 Edit token 機制完全相同!✅
網上聲稱 "faster than mutation" 的庫實際上是:
Mutative/Immer (Proxy-based):
- 比較對象: Naive FP (spread/copy)
- NOT 比原生 mutation 快
- 仍然是 O(n) 複製
Immutable.js (Persistent):
- 100x faster than naive FP (slice + push)
- NOT faster than mutation
- Structural sharing 優勢
結論: 沒有魔法讓 immutable 比 mutation 快。我們已經做得很好了。
-
Tree traversal overhead (最大因素)
- Mutation: O(1) direct array access
- Pura: O(log₃₂ n) tree navigation + bit-shifting
- 即使 log₃₂ 1000 ≈ 2,每次仍需 2-6 次 function call
-
Object allocation
- Mutation: 0 allocations
- Pura Transient: Still creates ~log n nodes per operation
- GC pressure
-
Function call overhead
- Each tree level = function call
- No tail-call optimization in JS
-
Indirection
- Array access:
arr[i]- single memory lookup - Tree access:
node.array[idx].array[idx]- multiple lookups
- Array access:
當前:
function getIndex(hash: number, shift: number): number {
return (hash >>> shift) & MASK;
}
// Called thousands of times
const index = getIndex(hash, shift);優化:
// Direct inline
const index = (hash >>> shift) & MASK;預期提升: 5-10% (減少 function call overhead)
當前:
const newChildren = [...node.children]; // Copy
newChildren[index] = newChild;優化:
const newChildren = new Array(node.children.length);
for (let i = 0; i < node.children.length; i++) {
newChildren[i] = node.children[i];
}
newChildren[index] = newChild;預期提升: 10-15% (V8 優化 monomorphic arrays)
當前 (HAMT setMut):
if (node.edit === edit) {
node.children.splice(arrayIndex, 0, newEntry); // Mutation
node.bitmap = setBit(node.bitmap, index);
return node;
}
// Copy
const newChildren = [
...node.children.slice(0, arrayIndex),
newEntry,
...node.children.slice(arrayIndex),
];問題: splice 仍然創建內部臨時數組
優化:
if (node.edit === edit) {
// Pre-allocate exact size
const len = node.children.length;
const arr = new Array(len + 1);
for (let i = 0; i < arrayIndex; i++) arr[i] = node.children[i];
arr[arrayIndex] = newEntry;
for (let i = arrayIndex; i < len; i++) arr[i + 1] = node.children[i];
node.children = arr;
node.bitmap = setBit(node.bitmap, index);
return node;
}預期提升: 5-10%
概念: Size < 32 的 List 直接用 array,不建 tree
當前:
IList.of(1, 2, 3) // 創建 root + tail nodes優化:
// Special case for small lists
if (size <= 32) {
return new IList({ type: 'flat', array: [...items] });
}優勢:
- 小 List (< 32 elements): 接近原生性能
- 常見場景優化 (大多數 List 都很小)
預期提升: Small lists 50%+, overall 20-30%
概念: Sequential access 很常見,cache 最後訪問的節點
class IList<T> {
private lastAccessIndex?: number;
private lastAccessNode?: LeafNode<T>;
get(index: number): T | undefined {
// Cache hit
if (this.lastAccessNode &&
index >= this.lastAccessIndex! &&
index < this.lastAccessIndex! + 32) {
return this.lastAccessNode.array[index & 0x1f];
}
// Cache miss - traverse and cache
const result = Vector.get(this.root, index);
// ... update cache ...
return result;
}
}預期提升: Sequential access 30-50%
Concept: Optimize common patterns
// Fast path for map.set() when key doesn't exist
set(key: K, value: V): IMap<K, V> {
const keyHash = HAMT.hash(key);
// Fast path: empty map
if (this.size === 0) {
return new IMap({ type: 'entry', key, value, hash: keyHash }, 1);
}
// Normal path
// ...
}預期提升: 5-10% for common patterns
將 tree traversal 用 WebAssembly 實現:
優勢:
- 更快的位運算
- 更好的內聯
- 更少的 GC
風險:
- 複雜度大增
- Bundle size 增加
- 需要維護兩套代碼
預期提升: 30-50% for tree operations
概念: Reuse node objects instead of creating new
class NodePool {
private pool: BranchNode<any>[] = [];
allocate(): BranchNode<any> {
return this.pool.pop() || { type: 'branch', array: [], edit: undefined };
}
release(node: BranchNode<any>): void {
node.array = [];
node.edit = undefined;
this.pool.push(node);
}
}優勢: 減少 GC pressure
風險: 記憶體洩漏風險,複雜度增加
預期提升: 10-20%
- ✅ Inline bit operations
- ✅ Pre-allocate arrays in hot paths
- ✅ Remove unnecessary object creation in transient
預期: 20-30% overall improvement 新 gap: List build 3.4x, Map build 2.0x
- ✅ Flat array optimization for small collections
- ✅ Cache last access for sequential patterns
- ✅ Specialized fast paths
預期: Additional 30-40% improvement 新 gap: List build 2.4x, Map build 1.4x
- ❓ WASM for critical paths
- ❓ Object pooling
- ❓ Custom memory layout
預期: Additional 30-50% 新 gap: List build 1.6x, Map build 1.0x (接近 mutation!)
Fundamental overhead that CANNOT be eliminated:
-
Structural sharing 成本
- Mutation: 直接修改
- Persistent: 必須複製 path (至少 O(log n) nodes)
-
Tree navigation
- Mutation:
arr[i]- 1 operation - Persistent: 2-6 function calls + bit operations
- Mutation:
-
Memory allocation
- Mutation: 0 allocations
- Persistent: 至少創建 log n 個 node objects
理論最佳情況: ~2x slower than mutation
這是 immutability 的必然代價。
- ✅ 已經實現所有標準優化 (transient, tail buffer, etc.)
- ✅ 與 Immutable.js 相同的優化策略
- ✅ 比 naive FP 快 40-742x
⚠️ 比 mutation 慢 3-20x
- Phase 1 優化: 20-30% improvement → 2-14x gap
- Phase 2 優化: 30-40% improvement → 1.4-10x gap
- Phase 3 優化: 30-50% improvement → 1-7x gap
最佳情況: ~2x slower than mutation (with heroic optimizations)
- 先實施 Phase 1: 低風險高回報
- 評估 Phase 2: 根據實際需求決定
- 謹慎 Phase 3: 成本效益比不高
Persistent data structures 永遠不會比 mutation 快。我們的目標是:
- ✅ 比 naive FP 快很多 (已達成)
- ✅ 與 mutation 差距可接受 (已達成)
- ✅ 提供 immutability 價值 (safety, time-travel, etc.)
3-5x slower 是可接受且合理的 trade-off for immutability benefits.
我們無法直接比較,因為:
- Immutable.js 沒有公開的 vs mutation benchmarks
- 不同的測試環境
- 不同的實現細節 (他們用 JS,我們用 TS)
但從架構來看:
- 相同: Vector Trie, 32-way branching, owner ID
- 差異: 我們有 tail buffer optimization,他們沒有明確提到
推測: 性能應該在同一量級 (±20%)
- 安裝 Immutable.js
- 在相同環境下跑 benchmark
- 對比具體數據
- 學習他們的特定優化
- Inline hot path bit operations
- Pre-allocate arrays instead of spread
- Remove splice in transient, use manual loop
- Flat array for size <= 32
- Last access cache
- Benchmark vs Immutable.js
- WASM evaluation
- Object pooling POC
- Memory layout optimization
預期最終狀態: 2-7x slower than mutation (down from 3-20x) ✅