Anthropic 近期發表了一篇部落格文章,介紹如何完全使用 Claude 建構一個 C 編譯器。他們稱之為 CCC(Claude’s C Compiler),並聲稱它能夠編譯 Linux 核心。100% 的程式碼由 Claude Opus 4.6 編寫,人類僅透過編寫測試案例來引導整個過程。這聽起來足以讓人想測試其聲稱的真實性,並將 CCC 與業界標準 GCC 進行基準測試。

CCC 的原始程式碼可在 claudes-c-compiler 取得。它完全以 Rust 編寫,目標架構為 x86-64、i686、AArch64 和 RISC-V 64。前端、基於 SSA 的 IR、優化器、程式碼產生器、後置優化器、組合器、連結器和 DWARF 除錯資訊產生,全部都是從頭開始實作,沒有任何特定於編譯器的依賴。這對於 AI 來說是一項艱鉅的任務。

在我們開始比較之前,了解編譯 C 程式時會發生什麼是有幫助的。這個過程涉及四個階段。

圖片來源:gcc 編譯器的四個階段

編寫程式語言很難(先前的 vibe coding)。編寫編譯器更是難上加難。程式語言定義規則。編譯器必須理解這些規則,將它們翻譯成機器指令,優化輸出的速度和大小,處理不同 CPU 架構的邊緣案例,並每次都產生正確的程式碼。

GCC 自 1987 年開始開發。這將近 40 年的工作由數千名貢獻者完成。它支援數十種架構、數百種優化通道以及數百萬個數十年來發現並修復的邊緣案例。僅僅是優化通道(寄存器分配、函式內嵌、迴圈展開、向量化、死碼消除、常數傳播)就代表了多年的博士級研究。這也是它無處不在的原因之一。

這就是為什麼 CCC 能夠編譯真實的 C 程式碼本身就值得注意。但這也解釋了為何其輸出品質遠不及 GCC 的產出。建構一個能正確解析 C 的編譯器是一回事。建構一個能產生快速有效率機器碼的編譯器則是完全不同的挑戰。

諷刺的是,在四個階段中,編譯器(翻譯成組合語言)是 AI 最容易建構的部分。它主要涉及模式匹配和規則應用:接收 C 結構並將它們映射到組合語言模式。

組合器比看起來要難。它需要知道目標架構上每條指令的確切二進位編碼。僅 x86-64 就有數千種指令變體,具有複雜的編碼規則(REX 前綴、Mod/R/M 位元組、SIB 位元組、位移大小)。即使一個位元錯誤,CPU 也會執行完全出乎意料的行為。

連結器可以說是其中最難的。它必須處理重定位、多個物件檔之間的符號解析、不同的區段類型、位置無關程式碼、執行緒區域儲存、動態連結以及 ELF 二進位檔的格式特定細節。僅 Linux 核心的連結器腳本就有數百行佈局指令,連結器必須精確地遵循。

Linux 核心是世界上最複雜的 C 程式碼庫之一。它有數百萬行程式碼,使用 GCC 特定的擴展、內嵌組合語言、連結器腳本以及無數將編譯器推向極限的技巧。對於一個新的編譯器來說,它不是一個好的首要測試對象。

另一方面,SQLite 以單一的合併檔案(一個大的 .c 檔案)形式分發。它是標準 C,經過良好測試且獨立。如果你的編譯器能處理 SQLite,它就能處理很多東西。如果它無法正確處理 SQLite,那麼測試任何更大的東西就沒有意義。

這就是為什麼我同時測試了兩者。SQLite 告訴我們正確性和執行時間效能。核心則告訴我們規模和相容性。

CCC 是使用 gcc_m16 Cargo 功能建構的,該功能將 16 位元實模式啟動程式碼(-m16 旗標)委託給 GCC。這是必需的,因為 CCC 的 i686 後端產生的程式碼對於 32KB 的實模式限制來說太大了。x86_64 C 程式碼完全由 CCC 編譯。

一個 ccc_wrapper.sh 腳本將 .S 組合語言檔案導向 GCC(CCC 不處理組合語言),所有 .c 檔案則導向 CCC。

編譯器通常在以下場景下進行衡量。因此,測試也是圍繞這些場景設計的。

基準測試的設計是 CPU 密集型的:

公平的比較是 CCC 與 GCC 在 -O0(無優化)下的對比:CCC 花費 87 秒,而 GCC 花費 65 秒 – CCC 慢了 1.3 倍。「5 倍更快」的數字僅出現是因為 GCC 進行了 7 分鐘的優化工作,而 CCC 根本跳過了。

CCC 編譯了 Linux 6.9 核心中的每一個 C 原始程式碼檔案,沒有出現單一的編譯器錯誤(0 個錯誤,96 個警告)。對於一個完全由 AI 建構的編譯器來說,這確實令人印象深刻。

然而,建構在連結階段失敗,出現了大約 40,784 個未定義的參考錯誤。這些錯誤遵循兩種模式:

這些是 CCC 的重定位/符號產生中的連結器可見錯誤,而不是 C 語言編譯錯誤。這是一個很好的例子,說明了為何連結器是最難的部分。編譯器工作正常,但產生的重定位對於核心複雜的連結器腳本來說並不完全正確。

CCC -O0 和 -O2 產生位元組相同的二進位檔(4,374,024 位元組)。CCC 有 15 個 SSA 優化通道,但它們在每個優化級別都會運行。沒有分級優化 – -O 旗標被接受但完全被忽略。

當你要求 GCC 使用 -O2 編譯時,它會執行數十個額外的優化通道:

GCC 的 -O2 花費 7 分鐘完成這項工作,而回報是明顯的:產生的二進位檔執行速度快了 1.7 倍(6.1 秒對比 10.3 秒)。

CCC 在任何優化級別都不會執行這些操作。比較「CCC 編譯時間 vs GCC -O2 編譯時間」就像比較一個只能列印黑白的印表機與一個可以列印全彩的印表機。黑白印表機速度更快,但它沒有做相同的工作。

CCC 編譯的 SQLite 在功能上是正確的 – 它產生與 GCC 編譯的 SQLite 相同的查詢結果。所有 5 個崩潰/邊緣案例測試都通過了。但它非常慢。

在這些測試中沒有觀察到失敗:

每個查詢的細分顯示 CCC 的減速並非均勻。簡單查詢僅慢 1-7 倍,但涉及巢狀迴圈的複雜操作卻會爆炸性地變慢:

模式很清楚:涉及巢狀迭代(子查詢、JOINs)的操作速度會慢幾個數量級,而簡單的順序操作僅慢一點。

現代 CPU 有一組稱為寄存器的快速儲存位置。好的編譯器會嘗試將常用變數保留在這些寄存器中。當變數多於寄存器時,編譯器會將它們「溢出」到堆疊(常規 RAM),這速度慢得多。

CCC 最嚴重的效能問題是過度的寄存器溢出。SQLite 的核心執行引擎 sqlite3VdbeExec 是一個單一函式,包含 100 多個局部變數和一個巨大的 switch 語句。CCC 沒有好的寄存器分配,因此它將幾乎所有變數溢出到堆疊。

GCC -O0(383 行,使用堆疊但效率高):

CCC(1,189 行 – 3.1 倍程式碼):

CCC 對一個有 32 個變數的函式使用高達 -0x2ae8(11,000 位元組深)的堆疊偏移。每個操作都經歷:堆疊 -> rax -> 堆疊,使用 %rax 作為中轉寄存器。

對於寄存器密集型程式碼,CCC 比 GCC O0 慢 4.2 倍。在具有 100 多個變數和 200 多個 switch 分支的 sqlite3VdbeExec 中,這個比例會累積到 100 倍以上。

CCC 在所有優化級別都運行相同的 15 個通道的 SSA 流程:

這意味著 -O2 提供了零好處。CCC 產生的每個二進位檔實際上都是 -O0 品質,無論你傳遞什麼旗標。

正在執行的 CCC 編譯的 SQLite 的 GDB 堆疊追蹤顯示損壞的框架資料:

2.78 倍的程式碼膨脹意味著更多的指令快取遺漏,這加劇了寄存器溢出的懲罰。

CCC 編譯的二進位檔缺乏內部函式符號(nm 報告 0 個符號,readelf 顯示只有 90 個 PLT stubs,而 GCC 有 1,500 多個函式)。這使得剖析和除錯變得不可能。

NOT IN(子查詢)模式導致 SQLite 執行巢狀迴圈:對於外部表格中的大約 100,000 行中的每一行,它會掃描內部表格中的大約 10,000 行。這大約是 SQLite 主要執行函式(sqlite3VdbeExec)的 10 億次迭代,這基本上是一個巨大的 switch 語句。

由於 CCC 大約 4 倍的每次迭代開銷來自寄存器溢出,加上 2.78 倍更大的二進位檔(CPU 無法將所有指令保留在其快速快取中)導致的額外快取遺漏,減速會累積:

這就是為什麼簡單查詢(INSERT、DROP TABLE)僅慢 1-2 倍,但巢狀操作卻慢了 100,000 倍以上。

在 Anthropic 發布 CCC 的數小時內,有人開啟了 issue #1 – 「Hello world 無法編譯」。README 中的範例在新的 Fedora 或 Ubuntu 安裝上無法正常工作:

同時,GCC 卻能順利編譯。問題在於 CCC 的預處理器沒有為 stddef.h 和 stdarg.h 搜尋正確的系統 include 路徑(這些來自編譯器,而不是 C 函式庫)。它獲得了 288 個讚,超過 200 條評論,並演變成一個傳奇的 GitHub 討論串,人們標記 @claude 要求它修復錯誤,要求 @grok 進行摘要,並發表「我的工作保住了」之類的評論。

有人在 Compiler Explorer 上使其工作,並評論說組合語言輸出「讓我想起了大學生編譯器作業的品質」。坦白說,這既刻薄又不完全錯誤,當你看到寄存器溢出的模式時。

截至撰寫本文時,該問題仍然開放。

Claude 的 C 編譯器是一項了不起的成就。它是一個完全由 AI 建構的、可工作的 C 編譯器,能夠正確編譯 Linux 核心的 2,844 個檔案,沒有單一錯誤。它產生功能正確的程式碼(透過 SQLite 驗證 – 所有查詢都返回正確結果,所有崩潰測試都通過)。

輸出的程式碼非常慢。CCC 編譯的 SQLite 花費 2 小時運行一個 GCC 在 10 秒內完成的基準測試。根本原因是寄存器分配不佳 – CCC 使用單一寄存器作為中轉,將值在堆疊位置之間移動,將每個操作變成多次記憶體存取。

「編譯核心」的說法需要一個附註。CCC 編譯了所有 C 原始程式碼檔案,但最終的二進位檔無法產生,因為 CCC 為核心資料結構(__jump_table、__ksymtab)產生了不正確的重定位。

優化旗標只是裝飾。將 -O2 或 -O3 傳遞給 CCC 實際上什麼也沒做 – 輸出的二進位檔與 -O0 的位元組相同。

對於 Anthropic 展示 Claude 可以建構複雜軟體的既定目標,CCC 是一個真正的成功。對於任何想要編譯軟體以實際高效運行的人來說,GCC(或 Clang,或任何生產級編譯器)仍然是唯一的真正選擇。

所有腳本、結果和圖表都可在 compare-claude-compiler 取得。

這項工作的一部分得到了 AI 的協助。用於生成基準測試結果和圖表的 Python 腳本是透過 AI 協助編寫的。基準測試設計、測試執行、分析和寫作由人類完成,並在需要時由 AI 提供協助。