深度对比 AI 生成的 C 编译器与 GCC 的功能和性能差异,实测评估 Claude 代码生成的能力边界。
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编译器的四个阶段
预处理器:处理#include、#define和其他指令。它获取源代码并生成展开后的源代码。
编译器:获取预处理后的源代码并将其翻译成汇编语言。这是真正的重头戏,涉及理解C语言、类型检查、优化、寄存器分配等。
汇编器:将汇编语言转换为机器代码(目标文件)。它必须了解目标CPU架构的精确指令编码。
链接器:获取一个或多个目标文件并将它们组合成单个可执行文件。它解决文件之间的引用,设置内存布局并生成最终二进制文件。
编写编程语言很难(之前的凭感觉编码)。编写编译器则完全是另一个层次。编程语言定义规则。编译器必须理解这些规则,将它们转换成机器指令,优化输出以提高速度和大小,处理不同CPU架构之间的边界情况,并每次都生成正确的代码。
GCC自1987年以来一直在开发中。这接近40年来数千名贡献者的工作。它支持几十种架构、数百个优化通道和数百万个在几十年中被发现和修复的边界情况。仅优化通道(寄存器分配、函数内联、循环展开、向量化、死代码消除、常量传播)就代表了多年的博士级研究。这是它无处不在的原因之一。
这就是为什么CCC能够编译真实C代码本身就值得注意。但它也解释了为什么输出质量远不如GCC产生的质量。构建能够正确解析C的编译器是一回事。构建能够生成快速高效机器代码的编译器是完全不同的挑战。
具有讽刺意味的是,在四个阶段中,编译器(翻译成汇编)是最容易被AI构建的部分。它主要涉及模式匹配和规则应用:获取C构造并将其映射到汇编模式。
汇编器看起来比它更难。它需要了解目标架构每条指令的精确二进制编码。仅x86-64就有数千种指令变体,具有复杂的编码规则(REX前缀、ModR/M字节、SIB字节、位移大小)。即使只出错一位也意味着CPU会执行完全意外的操作。
链接器可以说是最难的。它必须处理重定位、跨多个目标文件的符号解析、不同的节类型、位置无关代码、线程本地存储、动态链接和ELF二进制文件的格式特定细节。Linux内核链接器脚本本身就有数百行布局指令,链接器必须准确处理。
Linux内核是世界上最复杂的C代码库之一。它有数百万行代码,使用GCC特定的扩展、内联汇编、链接器脚本和无数将编译器推向其极限的技巧。它不是新编译器的好的第一个测试。
另一方面,SQLite以单个amalgamation文件(一个大.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秒vs 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秒vs 10.3秒)。
CCC在任何优化级别都不做这些。比较"CCC编译时间vs GCC -O2编译时间"就像比较一台只打印黑白的打印机与一台做全色打印的打印机。黑白打印机更快,但它没有做相同的工作。
CCC编译的SQLite在功能上是正确的 – 它生成与GCC编译的SQLite相同的查询结果。所有5个崩溃/边界情况测试都通过了。但它非常慢。
这些测试中未观察到故障:
按查询分解显示CCC的放缓不均匀。简单查询仅慢1-7倍,但涉及嵌套循环的复杂操作会急剧增加:
模式很清楚:涉及嵌套迭代的操作(子查询、JOIN)要慢几个数量级,而简单顺序操作仅略微变慢。
现代 CPU 有一组称为寄存器的小型高速存储位置。优秀的编译器会尽量将频繁使用的变量保存在这些寄存器中。当变量数量超过寄存器数量时,编译器会将其"溢出"到栈(常规 RAM),这要慢得多。
CCC 最大的性能问题是过度的寄存器溢出。SQLite 的核心执行引擎 sqlite3VdbeExec 是一个包含 100 多个局部变量和巨大 switch 语句的单一函数。CCC 没有良好的寄存器分配机制,因此它将几乎所有变量都溢出到栈中。
GCC -O0(383 行,使用栈但效率高):
movl -8(%rbp), %eax ; 加载循环计数器
cmpl -36(%rbp), %eax ; 与 n 进行比较
jl .L6 ; 分支跳转
movl (%rax), %edx ; 直接加载 a[i]
cmpl %eax, %edx ; 在寄存器中比较
CCC(1,189 行 – 代码多 3.1 倍):
movq -0x1580(%rbp), %rax ; 从深栈偏移加载
movq %rax, -0x2ae8(%rbp) ; 存储到另一个深栈偏移
movq -0x1588(%rbp), %rax ; 加载下一个值
movq %rax, -0x2af0(%rbp) ; 存储到下一个偏移
; ... 还有数十个内存到内存的复制操作
CCC 对一个只有 32 个变量的函数使用深达 -0x2ae8(11,000 字节深)的栈偏移。每个操作都是:栈 -> rax -> 栈,使用 %rax 作为穿梭寄存器。
CCC: 8.604s (比 GCC -O2 慢 41.6 倍)
GCC O0: 2.026s (CCC 慢 4.2 倍)
GCC O2: 0.207s (基准线)
对于寄存器密集型代码,CCC 比 GCC -O0 慢 4.2 倍。在具有 100 多个变量和 200 多个 switch 分支的 sqlite3VdbeExec 中,这个比率会复合到 100 倍以上。
CCC 在所有优化级别都运行相同的 15 遍 SSA 管道:
$ diff <(ccc -O0 -S test.c -o -) <(ccc -O2 -S test.c -o -)
(没有区别)
这意味着 -O2 毫无好处。CCC 生成的每个二进制文件实际上都是 -O0 质量,无论你传递什么标志。
GDB 对运行的 CCC 编译的 SQLite 的栈跟踪显示损坏的帧数据:
#0 0x000000000054fffb in ?? ()
#1 0x0000000000000001 in ?? () <- 不是有效的返回地址
#2 0x00000000108b73b8 in ?? () <- 堆地址,不是栈
CCC 不会生成正确的帧指针链,使调试变成不可能。
2.78 倍的代码膨胀意味着更多指令缓存未命中,这加剧了寄存器溢出的惩罚。
CCC 编译的二进制文件缺少内部函数符号(nm 报告 0 个符号,readelf 只显示 90 个 PLT 存根对比 GCC 的 1,500 多个函数)。这使分析和调试变成不可能。
NOT IN (subquery) 模式导致 SQLite 执行嵌套循环:对于外表中大约 100,000 行中的每一行,它都要扫描内表中大约 10,000 行。这大约是通过 SQLite 主执行函数(sqlite3VdbeExec)的 10 亿次迭代,这基本上是一个巨大的 switch 语句。
由于 CCC 因寄存器溢出导致的每次迭代大约 4 倍开销,加上 2.78 倍更大二进制文件导致的额外缓存未命中(CPU 无法将所有指令保留在其快速缓存中),减速会复合:
这就是为什么简单查询(INSERT、DROP TABLE)只慢 1-2 倍,但嵌套操作会爆炸到 100,000 倍以上减速的原因。
正确性:编译了内核中的每个 C 文件(0 个错误),并为所有查询生成了正确的 SQLite 输出
稳定性:在所有测试中零崩溃、零段错误
GCC 兼容性:接受 GCC 命令行标志,作为编译的直接替代品
运行时性能:复杂操作上慢 737 倍到 158,000 倍
链接器兼容性:为内核的 __jump_table 和 __ksymtab 部分生成不正确的重定位
代码大小:由于寄存器溢出,二进制文件大 2.7-3 倍
内存使用:编译使用 5.9 倍更多 RAM(1.6 GB vs SQLite 的 272 MB)
没有优化层级:-O0 到 -O3 生成相同输出
没有调试信息:缺少 DWARF 数据、破损的帧指针、没有函数符号
编译速度:只能与 -O0 比较,因为 CCC 不做任何超出此范围的事。CCC 大约慢 25% 对比 GCC(87s vs 65s)
Anthropic 发布 CCC 的几小时内,有人开了议题 #1 – "Hello world 无法编译"。README 中的示例在全新的 Fedora 或 Ubuntu 安装上不起作用:
$ ./target/release/ccc -o hello hello.c
/usr/include/stdio.h:34:10: error: stddef.h: No such file or directory
/usr/include/stdio.h:37:10: error: stdarg.h: No such file or directory
ccc: error: 2 preprocessor error(s) in hello.c
同时,GCC 编译它毫无问题。问题是 CCC 的预处理器没有在 stddef.h 和 stdarg.h 的正确系统包含路径中搜索(这些来自编译器,而不是 C 库)。它获得了 288 个点赞、200 多条评论,成为那些传奇 GitHub 话题之一,人们标记 @claude 让它修复 bug,请 @grok 做摘要,发布"我的工作很安全"这样的评论。
有人在 Compiler Explorer 上让它工作,并评论汇编输出"让我想起大学生编译器作业的质量"。说实话,当你看到寄存器溢出的模式时,这既刺耳又不完全错误。
该议题在撰写本文时仍未解决。
Claude 的 C 编译器是一项卓越的成就。它是一个完全由 AI 构建的工作 C 编译器,可以正确编译 Linux 内核的 2,844 个文件,没有单一错误。它生成功能正确的代码(用 SQLite 验证 – 所有查询返回正确结果,所有崩溃测试通过)。
但它还未准备好用于真实用途:
输出代码非常慢。CCC 编译的 SQLite 花费 2 小时运行一个 GCC 在 10 秒内完成的基准测试。根本原因是寄存器分配不良 – CCC 使用单个寄存器作为穿梭来在栈位置之间移动值,将每个操作变成多次内存访问。
输出代码非常慢。CCC 编译的 SQLite 花费 2 小时运行一个 GCC 在 10 秒内完成的基准测试。根本原因是寄存器分配不良 – CCC 使用单个寄存器作为穿梭来在栈位置之间移动值,将每个操作变成多次内存访问。
"编译内核"的说法需要脚注。CCC 编译了所有 C 源文件,但最终二进制文件无法生成,因为 CCC 为内核数据结构(__jump_table、__ksymtab)生成了不正确的重定位。
"编译内核"的说法需要脚注。CCC 编译了所有 C 源文件,但最终二进制文件无法生成,因为 CCC 为内核数据结构(__jump_table、__ksymtab)生成了不正确的重定位。
优化标志是装饰性的。向 CCC 传递 -O2 或 -O3 从字面上什么都不做 – 输出二进制文件与 -O0 完全相同。
优化标志是装饰性的。向 CCC 传递 -O2 或 -O3 从字面上什么都不做 – 输出二进制文件与 -O0 完全相同。
对于 Anthropic 的既定目标——展示 Claude 可以构建复杂软件——CCC 是一项真正的成功。对于任何想要编译软件以实际高效运行的人来说,GCC(或 Clang,或任何生产编译器)仍然是唯一真正的选择。
两个 6+ vCPU、16+ GB RAM、50+ GB 磁盘的虚拟机
具有 build-essential、cargo、git、flex、bison、bc、libelf-dev、libssl-dev 的 Debian/Ubuntu
git clone https://github.com/anthropics/claudes-c-compiler
cd claudes-c-compiler
cargo build --release --features gcc_m16
# 内核
bash scripts/benchmark_kernel.sh gcc /usr/bin/gcc results/kernel_gcc
bash scripts/benchmark_kernel.sh ccc /path/to/ccc_wrapper.sh results/kernel_ccc
# SQLite
bash scripts/benchmark_sqlite.sh gcc gcc results/sqlite_gcc_v2
bash scripts/benchmark_sqlite.sh ccc /path/to/ccc_wrapper.sh results/sqlite_ccc_v2
python3 -m venv .venv
.venv/bin/pip install matplotlib numpy
.venv/bin/python scripts/analyze.py
所有脚本、结果和图表可在 compare-claude-compiler 获得。
这项工作的一部分得到了 AI 的协助。用于生成基准结果和图表的 Python 脚本是在 AI 协助下编写的。基准设计、测试执行、分析和写作由人工完成,AI 在需要的地方提供帮助。
⚠️ 评论由 Disqus 提供支持。你可能会在评论部分看到广告。