Cocoa with Love 最近做了一个很硬的实验:在 Apple Silicon 上,用纯 Swift 重写 Andrej Karpathy 的 llm.c。
llm.c 是一个约 1000 行、兼容 GPT-2 的 C 实现。测试模型约 1.244 亿参数,BT=464=256,单次训练迭代约 1.91×10¹¹ 次浮点运算。
反常点在这里:Swift 初版不是慢一点,而是慢很多。C 版 llm.c 约 0.175 training iterations/s,Basic Swift 只有约 0.014 iterations/s。
但我更在意的不是“Swift 能不能写 LLM”。作者也说得很清楚,真实项目应优先使用 Apple 已优化的 ML 和矩阵框架,比如 Accelerate、Metal Performance Shaders 这类路径。
这篇文章的价值更像拆机。
把框架拿掉以后,Swift 到底输在哪里?是语言本身,还是编译器、内存布局、浮点语义和芯片指令没有对齐?
矩阵乘法为什么一上来就决定输赢
LLM 训练的大头,不在花哨结构,而在大量矩阵计算。前向、反向、权重更新,最后都会落到反复执行的 z += x * y。
这也是作者从矩阵乘法路径下手的原因。它不是优化边角料,而是直接摸到训练吞吐的心脏。
早期数据可以先看这一张表:
| 实现 | tokens/s | training iterations/s | 直接判断 |
|---|---|---|---|
| llm.c(C,-O3) | 约 0.926 | 约 0.175 | 朴素 C 基线 |
| Basic Swift | 约 0.054 | 约 0.014 | 约为 C 的 7.3% |
| Swift + MutableSpan | 约 0.056 | 约 0.042 | 迭代速度升到约 C 的 24% |
这里有两个点容易看错。
第一,C 版并不是调用高度优化的生产级训练框架。它更像一个足够小、足够清楚的参照物。Swift 追的不是 PyTorch 后端,而是这条朴素 C 路线。
第二,Basic Swift 的慢,不能简单归因于“Swift 语言不适合性能计算”。原文抓到的热点更具体:Swift Array 写入会触发 _ArrayBuffer.beginCOWMutation()。
COW 是 copy-on-write。它能让日常 Swift 代码更安全、更省心。但在矩阵乘法内层循环里,哪怕没有真的复制,唯一性检查也会反复进入热点路径。
Swift 6.2 的 MutableSpan 让作者绕开这类 Array COW 检查。结果很直接:training iterations/s 从约 0.014 提到约 0.042。
这对 Swift 性能开发者的动作很明确:如果你在写音视频、科学计算、端侧 AI 或图形处理里的热循环,不要只盯算法名。先用 profiling 看 Array、边界检查、COW、临时对象有没有进热点。
很多时候,瓶颈不是“数学还不够高级”,而是数据每走一步都在交过路费。
Swift 初版慢在哪里:不只是语法,是编译器没拿到足够信号
C 还有一个很关键的优势:-ffast-math。
在矩阵乘法内层循环里,C 编译器可以更激进地放松浮点约束,生成 fused multiply-add,也就是 FMA。它把乘法和加法合成一类指令执行。
作者看了汇编。C 内核里大量出现 fmadd;Swift 初版则更像把乘法、移动、加法拆开做。指令更碎,吞吐自然上不去。
这不是一个小细节。
矩阵乘法的内层循环跑上亿、上千亿次。每次多几条指令,最后都会变成非常实在的时间差。
作者后面用 Swift-Numerics 的 Relaxed.multiplyAdd 替换普通的 a += b * c,让 Swift 触发 FMA 类优化。这个变化说明了一件事:Swift 不是完全不能接近底层性能,但你要把数值语义说给编译器听。
也就是说,性能差距来自一组具体变量:
| 变量 | C 路径 | Swift 初版问题 | Swift 优化方向 |
|---|---|---|---|
| 数组写入 | 裸内存访问更直接 | Array COW 检查进入热点 | MutableSpan 绕开部分开销 |
| 浮点优化 | 可用 -ffast-math | 普通写法不易触发 FMA | Relaxed.multiplyAdd 提供信号 |
| 内层循环 | 编译器更容易生成紧凑指令 | 指令形态更重 | 继续看 SIMD、展开、汇编形态 |
| 并行路径 | 可手写线程和向量化 | 仍需逐层验证 | 多线程、SIMD、Metal 逐级推进 |
这也是我不太买账“Swift 天生慢”这种说法的原因。它太省事,也不够准确。
更准确的说法是:Swift 默认给了你安全、抽象和工程友好;但在训练内核这种场景里,这些默认值可能变成成本。你要一层层把成本拆掉。
拆到最后,问题就从“语言之争”变成“编译器、内存和芯片能不能同向发力”。
对谁有用:不是普通用户,是写底层热路径的人
普通 AI 用户不用因为这篇文章改变选择。
如果目标是训练大模型,现实路径仍然更偏向 NVIDIA GPU 生态或云端集群。Mac 本地更常见的角色,是小模型实验、推理、教学、工具链开发,或者做一些受控规模的研究。
这篇文章真正影响的是两类人。
一类是 Swift 性能开发者。比如你在写音视频处理、图形计算、科学计算、端侧 AI 内核。看到这篇文章后,更现实的动作不是“用 Swift 手写训练框架”,而是调整排查顺序:先查 COW 和内存访问,再查 FMA 和编译器输出,然后才谈 SIMD、多线程和 Metal。
另一类是关注 Apple Silicon 性能边界的工程师。对他们来说,这篇文章提供了一条性能阶梯:CPU 标量循环只是起点,后面还有 SIMD、多核并行,甚至 GPU/Metal。每上一层,收益可能更大,工程成本也会更高。
限制也要说清楚。
目前材料只覆盖到早期优化和部分汇编分析,不能据此宣布 Swift 已经追平 C,更不能说纯 Swift LLM 训练已经进入生产可用。矩阵内核跑快,也不等于完整训练系统跑快。
中间还隔着内存带宽、缓存命中、线程调度、数值稳定性,以及不同 Apple Silicon 型号上的差异。
接下来最该看的不是一个漂亮的 Tflop/s 终点数字,而是三件事:
| 观察点 | 为什么重要 |
|---|---|
MutableSpan 和 Relaxed.multiplyAdd 的收益能否稳定复现 | 决定这是不是通用优化路线,而不是单个实验结果 |
| SIMD、多线程、Metal 每一层各自带来多少增益 | 决定 Swift 路线离 C 或框架后端还有多远 |
| 完整训练迭代是否保持收益 | 防止只优化了内核,系统吞吐却被调度和内存带宽吃掉 |
这才是这篇实验的主线。
它不是在劝大家离开框架。恰好相反,它让人更能看懂框架为什么值钱:成熟框架把内存、并行、指令和硬件后端都包好了。
离框架越远,控制权越大。代价也越清楚。
Swift 可以往底层走,但不能靠一句“现代语言也能快”走过去。它要靠每一个热点循环、每一条汇编、每一次内存访问来还账。
