上周调一个图片处理的小工具,瓶颈死死卡在解码这一步。我把同一张 1920×1080 的 QOI 图解码一万次,time 了一下:
$ time ./qoi_bench 10000
real 0m5.23s
user 0m5.21s
sys 0m0.02s
就一个图片解码,慢到我第一反应是"我是不是不小心编成 Debug 版了"。切回 ReleaseFast 重编,数字纹丝不动。行吧,不是我菜,是这活儿本来就这个速度。
后来在 GitHub 上翻到一个叫 dekoodaaja 的项目。名字是芬兰语,意思就是"解码器"。说白了,它干的事特别简单:把 QOI 的解码逻辑用 Zig 从头写了一遍,别的啥也不做,就图一个快。
项目地址:https://github.com/sorvi-platform/dekoodaaja
QOI 是个挺冷门的图片格式,全称 "Quite OK Image",当初的卖点就是实现简单、编解码快。官方参考实现是 C 写的,拢共三百来行——说白了,三百行以内,谁都能看懂它在干嘛,但真想让它跑得更快,就得开始跟编译器、跟内存较劲了。

它自带一个跑分脚本。我一开始偷懒,直接 git clone 然后 zig build,结果啥也没编出来——跑分入口写在 flake.nix 里,得走 Nix。行,装 Nix,再来:
$ nix run .#bench -- -Doptimize=ReleaseFast
qoi (image/qoi): 0.44 GB/s in, 6.17 GB/s out, 170.081us
170 微秒。换算成刚才那个一万次的循环,大概是 1.7 秒。同一台机器上,参考实现 phoboslab/qoi 是 523 微秒,也就是 5.2 秒。
同一个格式,同一台机器,快了差不多三倍。
把 README 里那张表完整拉下来(AMD Ryzen 7 3700X,ReleaseFast 模式):
| 实现 | 语言 | 解码吞吐 | 单张耗时 |
|---|---|---|---|
| dekoodaaja | Zig | 0.44 GB/s | 170µs |
| qoicoubeh | Rust | 0.34 GB/s | 221µs |
| qoi-rust | Rust | 0.32 GB/s | 234µs |
| rapid-qoi | Rust | 0.32 GB/s | 236µs |
| magicqoi | Rust | 0.28 GB/s | 272µs |
| phoboslab/qoi(参考实现) | C | 0.14 GB/s | 523µs |

表里第二到第四名清一色是 Rust 写的,它们和参考实现之间也没拉开多大差距。结果一个 Zig 版本直接把第一名线拉出去一截,这个我是真服。
那它凭什么快?
QOI 的解码本身是个极简状态机:读一个字节,判断它属于哪一类操作(索引、差分、游程、原样像素),然后更新一个记着最近 64 个像素的小数组。说白了,整段逻辑就一个热循环,快慢全压在"每次数组访问要不要做边界检查""这个分支能不能被 CPU 预测""内存能不能一次搬够"这些最底层的事情上。
Zig 的 ReleaseFast 把这些运行时检查关掉了,作者又针对性地重排了循环,速度就是这么一微秒一微秒抠出来的。原理我大概知道个轮廓,具体到某一行优化的取舍,说实话我没完全啃懂,有懂的大佬欢迎评论区指正。
不过,夸之前,有几个我自己挺在意的地方。
第一,它现在只支持 QOI。README 开头自己就写着 "(Currently only QOI)"——除了这个冷门格式,你拿它没别的用处。
第二,README 简陋得有点过分。没有安装说明,没有用法示例,没有 API 文档,翻到底就一张 benchmark 表。想接进自己的项目,你基本得去读源码。简介那句 "Providers fast decoders" 还漏了个 s。
第三,这次的"最快"是有前提的。仓库里还有一份 ReleaseSmall 模式的跑分——那个模式优先体积而不是速度:dekoodaaja 掉到 0.20 GB/s,Rust 写的 rapid-qoi 反倒以 0.25 GB/s 反超了它。
这条对自己不利的数据,作者也原样贴在 README 里了。这点得夸一句。
再叠上 12 个 Star、0 个 Fork、26 次提交这个体量,我的判断是:东西是好东西,但别急着往关键路径上押。
我眼下只是把它当个 benchmark 参照,还没塞进生产管线。要是你手上的项目里,QOI 解码正好是个性能热点,值得拉下来跑一遍;如果你只是偶尔转那么几张图,那三百行 C 的官方实现更省心,也更好维护。
想顺着往下看的话,可以去翻 phoboslab/qoi,格式规范和参考实现都在里面;想留在 Zig 生态里的,ikskuh/zig-qoi 是另一个选择,跑分上比 dekoodaaja 慢一截,但它是纯 Zig 包,接进项目更方便。
我最近还在折腾几个 Zig 写的小工具,回头一并整理成一篇。你要是也在拿 Zig 做点什么,评论区吱一声,我挺好奇大家都在拿它干嘛。
咖啡凉了,我回去继续对着 profiler 发呆。回见。