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/straining 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普通写法不易触发 FMARelaxed.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 终点数字,而是三件事:

观察点为什么重要
MutableSpanRelaxed.multiplyAdd 的收益能否稳定复现决定这是不是通用优化路线,而不是单个实验结果
SIMD、多线程、Metal 每一层各自带来多少增益决定 Swift 路线离 C 或框架后端还有多远
完整训练迭代是否保持收益防止只优化了内核,系统吞吐却被调度和内存带宽吃掉

这才是这篇实验的主线。

它不是在劝大家离开框架。恰好相反,它让人更能看懂框架为什么值钱:成熟框架把内存、并行、指令和硬件后端都包好了。

离框架越远,控制权越大。代价也越清楚。

Swift 可以往底层走,但不能靠一句“现代语言也能快”走过去。它要靠每一个热点循环、每一条汇编、每一次内存访问来还账。