為什麼 Zig 編譯器程式碼那麼小?
Zig 編譯器的程式碼量確實比其他主流編譯器小很多,這背後有幾個關鍵設計決策。
1. 程式碼量對比
Zig 編譯器: ~30 萬行程式碼
Rust 編譯器: ~200 萬行程式碼
LLVM: ~1000 萬行程式碼
GCC: ~1500 萬行程式碼
Clang: ~100 萬行程式碼
2. 核心設計哲學
🎯 簡單勝於複雜(Simplicity over Complexity)
Zig 的設計原則:
- 沒有隱藏控制流:沒有異常、沒有隱式記憶體分配
- 沒有巨集系統:不需要複雜的巨集展開引擎
- 沒有運算子重載:減少語意分析複雜度
- 沒有型別推導:明確的型別系統更容易實作
// Zig:明確、簡單
const allocator = std.heap.page_allocator;
const memory = try allocator.alloc(u8, 100);
defer allocator.free(memory);
#![allow(unused)] fn main() { // Rust:複雜的所有權系統需要更多編譯器邏輯 let mut vec = Vec::new(); vec.push(1); // 編譯器需要追蹤 lifetime、borrowing、ownership }
3. 技術實現策略
A. 直接使用 LLVM 後端
Zig 不自己實作程式碼生成:
其他編譯器架構:
Source → Lexer → Parser → AST → IR → 優化 → 程式碼生成 → Machine Code
↑ 自己實作這些部分
Zig 架構:
Source → Lexer → Parser → AST → LLVM IR → (交給 LLVM 處理)
↑ 只做到這裡
節省的程式碼量:
- ✅ 不需要實作多平臺程式碼生成器
- ✅ 不需要實作複雜的優化 pass
- ✅ 不需要維護平臺相關的組合語言輸出
B. 沒有複雜的型別系統
// Zig:簡單的型別系統
fn add(a: i32, b: i32) i32 {
return a + b;
}
對比 Rust 編譯器需要處理的複雜度:
#![allow(unused)] fn main() { // Rust:需要處理 lifetime、trait bounds、泛型約束 fn add<T>(a: T, b: T) -> T where T: std::ops::Add<Output = T> + Copy { a + b } // 編譯器需要: // 1. Trait resolution(特徵解析) // 2. Lifetime inference(生命週期推導) // 3. Borrow checker(借用檢查器) // 4. Monomorphization(單態化) }
Zig 編譯器省略的功能:
- ❌ 沒有 Trait 系統
- ❌ 沒有 Lifetime 分析
- ❌ 沒有 Borrow Checker
- ❌ 沒有複雜的泛型推導
C. Comptime:編譯期執行,而非複雜推導
Zig 的天才設計:用編譯期執行取代複雜的型別系統
// Zig:直接在編譯期執行程式碼
fn max(comptime T: type) type {
return struct {
pub fn get(a: T, b: T) T {
return if (a > b) a else b;
}
};
}
// 編譯器只需要:
// 1. 在編譯期執行這段程式碼
// 2. 生成結果
// 不需要複雜的型別推導引擎!
對比 C++ 的模板系統:
// C++:需要複雜的 template instantiation 引擎
template<typename T>
T max(T a, T b) {
return a > b ? a : b;
}
// 編譯器需要:
// 1. Template parsing
// 2. SFINAE (Substitution Failure Is Not An Error)
// 3. Template specialization resolution
// 4. Two-phase lookup
// 這些都需要大量程式碼實作!
D. 自舉(Self-Hosting)的優勢
Zig 編譯器是用 Zig 寫的:
// Zig 編譯器本身就是 Zig 程式
// 可以使用 Zig 的 comptime 功能來簡化實作
const Parser = struct {
tokens: []Token,
pos: usize,
// 編譯期生成 parsing 表格
const parse_table = comptime generateParseTable();
};
好處:
- ✅ 用 comptime 簡化編譯器邏輯
- ✅ 不需要寫兩次(bootstrapping compiler + 正式 compiler)
- ✅ 編譯器本身就是最好的測試案例
4. 與其他編譯器的對比
Rust 為什麼大?
#![allow(unused)] fn main() { // Rust 編譯器需要處理: // 1. Borrow Checker(最複雜的部分) fn process(data: &mut Vec<i32>) { // 編譯器需要追蹤每個變數的 lifetime // 驗證沒有 data race // 這需要大量程式碼! } // 2. Trait System impl<T: Display + Debug> MyTrait for T { } // 需要 trait coherence 檢查 // 需要 trait resolution 演算法 // 3. Async/Await async fn fetch() -> Result<String, Error> { } // 需要狀態機轉換 // 需要 Future trait 實作 }
C++ 為什麼大?
// C++ 需要支援 40 年的歷史包袱
// 1. 複雜的巨集系統
#define COMPLEX_MACRO(x) \
template<typename T> \
void func_##x(T t) { }
// 2. 多重繼承
class D : public A, public B, public C { };
// 3. Template Metaprogramming
template<int N>
struct Factorial {
static const int value = N * Factorial<N-1>::value;
};
5. Zig 的聰明取捨
❌ 不實作的功能
// 1. 沒有異常處理
// C++/Java 需要實作複雜的 exception unwinding
// Zig:用 error union
fn readFile() ![]u8 {
return error.FileNotFound;
}
// 2. 沒有運算子重載
// C++ 需要處理所有運算子的重載決議
// Zig:明確的函數呼叫
const result = vec.add(other_vec);
// 3. 沒有 OOP 繼承
// Java/C++ 需要虛函數表、動態分派
// Zig:用介面(interface)模式
const interface = MyInterface{
.ptr = &obj,
.vtable = &vtable,
};
6. 實際程式碼量統計
Zig 編譯器主要模組
src/
├── Ast.zig (語法樹, ~5K lines)
├── Sema.zig (語意分析, ~8K lines)
├── Codegen.zig (LLVM IR 生成, ~6K lines)
├── Type.zig (型別系統, ~4K lines)
├── Module.zig (模組系統, ~3K lines)
└── main.zig (主程式, ~2K lines)
總計:~30 萬行(包含標準庫)
對比 Rust
rustc/
├── typeck/ (型別檢查, ~50K lines)
├── borrowck/ (借用檢查, ~40K lines)
├── traits/ (Trait 系統, ~30K lines)
├── mir/ (中間表示, ~25K lines)
└── ... (還有很多)
總計:~200 萬行
7. 設計哲學的影響
Zig 的核心信念
"如果你能在編譯期執行它,就不需要特殊的語言功能"
// 範例:不需要特殊的 foreach 語法
// 用 comptime 自己實作!
pub fn forEach(comptime T: type, items: []T, func: fn(T) void) void {
for (items) |item| {
func(item);
}
}
// 使用
const numbers = [_]i32{1, 2, 3};
forEach(i32, &numbers, print);
結果:
- ✅ 語言功能少 → 編譯器簡單
- ✅ 但透過 comptime 保持強大
- ✅ 程式碼量小但功能不減
8. 總結:為什麼 Zig 能保持小巧
| 策略 | 節省的程式碼量 |
|---|---|
| 使用 LLVM 後端 | ~30萬行(優化+程式碼生成) |
| 沒有複雜型別推導 | ~20萬行(型別系統) |
| 沒有 Borrow Checker | ~50萬行(所有權分析) |
| 沒有異常處理 | ~10萬行(unwinding) |
| 沒有巨集系統 | ~15萬行(巨集展開) |
| Comptime 取代泛型 | ~25萬行(模板系統) |
核心理念:
簡單 ≠ 功能弱
簡單 = 更容易理解、維護、除錯
9. 對開發者的啟示
// Zig 的設計告訴我們:
// "正交性"(Orthogonality)的力量
// 不要:為每個需求加新功能
// 要:提供少數強大的基本功能,讓它們可以組合
// 範例:Zig 只有幾個核心概念
// 1. comptime(編譯期執行)
// 2. error union(錯誤處理)
// 3. 明確的記憶體分配
// 4. 簡單的型別系統
// 但這些可以組合出複雜功能!
結語
Zig 的小巧不是因為功能弱,而是因為聰明的設計:用簡單的機制組合出強大的功能,而不是為每個需求都加新的語言特性。這是非常值得學習的軟體設計哲學!🎯
本文檔探討了 Zig 程式語言編譯器如何透過精簡的設計哲學,在保持強大功能的同時維持小巧的程式碼基底。