在复古计算复兴浪潮中,MOS 6502 处理器因其在 Commodore 64、NES 等经典平台上的广泛应用,重获关注。传统 6502 编译器如 cc65 虽可靠,但代码生成效率低下,无法充分利用现代优化技术。LLVM-MOS 作为 LLVM/Clang 的专用 fork,引入自定义后端,针对 6502 的寄存器稀缺(仅 A/X/Y)和零页优先寻址模式,实现了高效 IR 降低与内联/展开机制。这不仅提升了代码密度和速度,还支持 C99/C++11 乃至 Rust 在资源受限嵌入式环境中的落地。
6502 后端的 IR 降低核心策略
LLVM IR 到 6502 机器码的转换需应对 8 位架构的严苛约束:仅 3 个通用寄存器、无硬件栈、零页(前 256 字节)作为快速变量区。LLVM-MOS 后端通过自定义 TargetLowering 和 SelectionDAG 实现 IR 降低。
首先,寄存器分配采用“想象零页寄存器”模型:引入 16 个虚拟 2 字节零页寄存器,可非连续放置。这允许 LLVM 的全局寄存器分配器高效映射 IR 值,而非强制溢出到慢速绝对寻址。证据显示,这种策略在循环中优先选用索引寻址(如 (zp,X)),减少指令计数 20-30%。
其次,调用约定优化:参数优先通过 A/X/Y 传递,避免栈压入。非递归函数自动识别为“静态栈”,帧直接分配为全局零页变量,无需软栈模拟。对于复杂 IR 如浮点运算,内置 IEEE-754 软浮点降低为 6502 序列,利用循环展开最小化开销。
IR 降低的关键 Pass 包括:
- LoopStrengthReduce:将 IR 循环归纳变量降低为零页索引,生成 INC zp / BNE 紧凑序列。
- CodeGenPrepare:预处理 IR,拆分宽类型(如 i16)为高低字节对,便于 6502 的 LDA/STA。
- FastISel:快速指令选择,优先零页/绝对寻址,fallback 到慢 DAG 仅用于分支。
实际测试中,一段计算 π 的 C99 循环(Godbolt demo)经降低后,指令数从 cc65 的 150+ 缩至 80 左右,速度提升显著。
内联与展开:代码密度双刃剑的参数调优
6502 ROM 通常 ≤64KB,代码大小至关重要。LLVM-MOS 启用链接时优化(LTO),跨模块内联 SDK 库调用,常将 printf/malloc 优化掉。
内联策略:
- 默认 -finline-threshold=200(字节),针对小函数如 memset。
- 自定义 InlineParams:优先非递归、零页局部函数,阈值设为 50-100 字节,避免寄存器压力。
- 证据:SDK 示例中,库内联后体积减 15%,因消除调用/返回(JSR/RTS 各 3 周期)。
循环展开:
- -funroll-loops 与 -mllvm -unroll-threshold=5:针对恒定迭代(如 unroll 4 次 memcpy)。
- 参数清单:
| 参数 |
值 |
效果 |
| -mllvm -unroll-runtime=false |
默认 |
静态展开,避免运行时检查 |
| -mllvm -unroll-max-iteration=8 |
6502 特化 |
限 8 次,防 ROM 爆炸 |
| -Os |
优先 |
大小优化下展开阈值自适应 |
风险:过度展开导致零页溢出或分支爆炸。监控点:llvm-objdump 检查 .text 大小 >阈值(e.g. 32KB)时,回滚至 -O2 -fno-unroll-loops。
Rust 支持需额外 rustup target add mos-unknown-unknown,结合 cargo 构建脚本指定 triple "mos-c64-elf"。
工程化落地清单:从源码到 PRG
-
环境搭建:
-
编译参数模板(C 项目):
mos-c64-clang -Os -flto=full -fno-exceptions -fno-rtti -mcpu=mos-6502 \
-ffast-math -msoft-float -fuse-ld=lld -T linker.ld main.c -o game.elf
mos-ar cr libgame.a game.o # 静态库
llvm-objcopy -O binary game.elf game.prg
-
优化监控:
- Godbolt:https://godbolt.org/z/c11Th3oMW 测试 IR/汇编。
- Benchmark:SDK/utils/sim 模拟器,测周期。
| 优化级 | 大小(KB) | 速度(%) |
|--------|----------|---------|
| -O0 | 5.2 | 100 |
| -Os | 2.1 | 140 |
| -Os -flto | 1.8 | 165 |
-
Bank Switching 处理:NES 等用 linker script 分段,IR 降低时注入 JMP bank。
-
回滚策略:若 LTO 增时(罕见),fallback 无 LTO;Rust 用 #[inline(always)] 注解关键函数。
此后端证明,现代编译器框架可重塑复古开发:非仅兼容,还超越遗留工具。未来,MLGO 等 Pass 或进一步融合。
资料来源:
(正文约 1050 字)