三全其美:AI代理用Rust和Java编写的HEIC解码器性能超越C库
每个接受照片上传的应用,迟早都会遇到 HEIC。iPhone 默认就以 HEIC 保存照片。Java 和 Rust 都无法开箱即用地读取它,而常见方案 libheif 是一个 C 库:每个容器镜像里都要带原生包,构建时需要 C 工具链,而且是由 C 写的解析器来读取陌生人上传的文件。我在一个 Java 产品和一个 Rust 产品中两次遇到这个问题。所以我走了另一条路:用各自语言本身编写解码器,由 AI 编程代理构建和改进,我负责掌舵。下面是过程。
1. 先让每个项目跑起来
每个代码库一个代理,各自从对应语言的现有开源解码器出发:Java 用 Openize.HEIC,Rust 用 HEIC decoder。最初的版本直接放在各自应用内部。它们能读取照片,这就足够发布了。因为已有上游库可作为起点,它们很快就准备好了,尽管为了对齐 libheif 还需补上一些功能。很容易就此停下。毕竟没人会抱怨一个能正常工作的上传功能。
2. 抽离出来并迭代
接下来,我让每个解码器抽离为独立库,开源,拥有自己的测试和发布流程,并遵循各自语言的最佳实践(也符合我的做法)。然后我让每个代理自行迭代:测量、改进、再测量。仅这一步就带来了很大进展。对于我们那十张测试照片,Java 解码器从 19 秒降到 3.2 秒。
[LOADING...]
3. 全面对比所有实现
各自单独推进时,每个代理最终都会耗尽显而易见的优化思路。真正改变局面的是,把两个解码器和 libheif 放在同一台机器上,用同样的照片,然后逐个阶段进行比较。每个差距都变成了一条线索。如果另一个实现把某一步做得很快,那就说明还有办法做得更快,而差距的大小告诉我是否值得追。差距就是附带了数字的提示。
[LOADING...]
对比也让我们保持诚实。它既显示了我们在哪些地方领先,也表明 libheif 的一部分领先来自做得更少:在我们解码器进行插值的地方,它直接复制了颜色样本。
4. 让代理互相交流
下面这一步带来了进一步提升。每个代理都在自己的会话中工作,我让它们与另一个代理交流。我让 Rust 代理向 Java 代理简要介绍自己的发现,反之亦然。一方已经弥合的差距,成了另一方的现成做法。Java 解码器采用了 Rust 解码器的熵解码方法;Rust 解码器则采用了 Java 解码器的并行 tile 和变换技巧。后来,这两个已经知道彼此存在的代理甚至自行协调,沟通各自可以在哪些 CPU 核心上跑基准测试。
[LOADING...]
把它们连接起来,是这个项目中最有用的一步。
5. 针对剩余差距重复上述过程
然后我们再次运行同样的循环:比较、找出剩余的最大差距、弥合它,然后再测量。Rust 这边又进行了一轮,弥合了与 libheif 在单核上的最后差距。Java 这边尝试了 JVM 的向量指令,又获得了 9% 到 14% 的提升。并非每个想法都保留下来。这个 C 库赖以闻名的技巧——宽向量指令——在 Rust 中几乎没带来收益:它的瓶颈在别处。我们测量了它,记录下来,然后放弃了它。
严格对待证据
计算机从不会闲着。浏览器、聊天应用、其他代理:所有东西都在争抢 CPU,数字也随之波动。当我们连续两次测量完全相同的代码时,第二次运行结果大约慢了 0.4%。有方向的噪声看起来和进展一模一样,或者和性能回退一模一样。所以我要求的是证据,而不是印象:统计 CPU 工作量而不是墙上时钟时间,交替运行新旧版本,按照片类型拆分结果,并丢弃任何小于噪声的“提升”。有几项改动没能通过。有一项改动总体上看起来不错,却拖慢了旋转照片——而手机上传的照片大多都是这类——直到代理找到了原因。
它们目前的表现
在同一台机器上、用同样的十张照片一起测量,三者做同样的工作:
- 8 核: Rust 0.46 秒,Java 0.55 秒,libheif 0.81 秒
- 单核: Rust 1.38 秒,libheif 1.45 秒,Java 1.96 秒
- Rust 还能对颜色进行插值以获得更锐利的边缘,并且仍与 libheif 持平:1.45 秒
内存占用与 libheif 相当或更低,并且与两个解码器最初的水平相比已进步巨大。
[LOADING...]
我的收获
- 独自迭代能让你走得很远。对比能让你走得更远。 两个实现之间的差距就是最好的待办清单。
- 让代理交叉借鉴很有帮助。 这些想法本来就在两个独立会话中存在。让它们互相交流触发了更多优化。
- 严格对待证据。 要求测量结果能够经受噪声的考验,并准备好丢弃经不起考验的改动。
- 两种语言的两个实现胜过一个。 每个实现都让另一个更快。把第三个实现(libheif)作为参照帮助更大。
- Java 也很快。“Java 很慢”是一个梗,而不是测量结果。以正确的方式优化,它在单核上接近 C 库,在八核上超过 C 库。
两个解码器都是开源的: