Roc 对比 C:roc-deflate 在 Apple Silicon 上基准测试与 libdeflate 相差 2.6%
一个完全用纯函数式语言 Roc 编写的 DEFLATE 压缩包,在 2021 款 M1 MacBook Pro 上运行速度中位数比经过 SIMD 优化的 C 语言库 libdeflate 慢 2.6%,在运行 Ubuntu 的 AMD Ryzen 9 3900X 上慢 8.9%。Roc 的创建者 Richard Feldman 于 2026 年 10 月 22 日发布了该基准测试,并将这一结果描述为他事先"没想到"的:一种具有自动内存管理、边界检查且没有不安全逃生舱口的函数式语言,与一个没有其他语言能匹敌的手工调优 C 实现相比,差距保持在个位数百分比以内。
基准测试概述
roc-deflate 是由 Niclas Ahden 编写并用于付费生产工作的 DEFLATE 包,在 Silesia 压缩语料库上与 libdeflate 进行了基准测试。两个实现被验证能产生相同的输出,并在 L1 到 L12 压缩级别上运行了 144 个基准案例。仅比较了非流式 API,因为 roc-deflate 尚未实现流式处理。报告的数字是 M1 MacBook Pro 上 15 次运行和 Ryzen 3900X 上 7 次运行的中位数。主要数据:roc-deflate 在 M1 上中位数慢 2.6%,在 Ryzen 3900X 上慢 8.9%。
Roc 比 C 的 libdeflate 更快还是更慢?
Roc 平均较慢,但差距很小,且 roc-deflate 在某些输入上胜出。在整个 144 案例套件中,中位数减速在 Apple Silicon 上为 2.6%,在 x64 上为 8.9%。文章报告称,在最坏情况下 roc-deflate 比 libdeflate 慢 18%,而在最佳情况下则快略超 18%。这些胜利并非 C 无法实现的优化的产物;两个实现只是不同,因此特定输入的结果不同。从 libdeflate 切换到 roc-deflate 可能在某些输入上带来加速,但平均而言不会。
来自 M1 机器在压缩级别 L1 下的代表性数据,15 次运行的中位数:
- 压缩
dickens文件:libdeflate 230.57 MB/s,roc-deflate 202.92 MB/s。 - 压缩
nci文件:libdeflate 601.03 MB/s,roc-deflate 529.49 MB/s。 - 压缩
sao文件:libdeflate 160.21 MB/s,roc-deflate 145.28 MB/s。
来自 Ryzen 3900X 在 L1 下的代表性数据,7 次运行的中位数:
- 压缩
dickens:libdeflate 191.10 MB/s,roc-deflate 182.30 MB/s。 - 压缩
nci:libdeflate 387.30 MB/s,roc-deflate 428.60 MB/s。 - 压缩
sao:libdeflate 157.40 MB/s,roc-deflate 138.50 MB/s。
纯函数式语言是否付出性能代价?
Roc 的性能上限低于 C,但代价比预期小,因为编译器会移除它能证明不必要的检查。Roc 代码完全内存安全,使用自动引用计数,对动态大小的集合执行边界检查,并且不提供 Rust 风格的 unsafe 关键字来退出任何这些机制。文章明确指出 C 的性能上限高于 Roc:对于 roc-deflate 目前胜过 libdeflate 的任何输入,C 代码都能缩小这一差距,反之则不然。roc-deflate 接近 C 性能只是因为 Roc 的优化管线确定许多边界检查和引用计数操作可以安全地消除。
这是 Roc 首次被测量到如此接近调优的 C 库。文章明确表示一个结果不能推广:据作者所知,这是对任何 Roc 包投入的最大优化努力,因此这种方法如何转移到其他代码库完全是未知数。
剩余个位数差距带来的好处
文章通过列出纯 Roc 代码提供的四项保证(C、Zig 和 Rust 代码无法提供)来论证这一权衡是值得的。这些在语言中都无法选择退出。
内存安全
roc-deflate 无法访问那种未检查的内存操作,这种操作曾导致 Node.js 2026 年 10 月安全更新中披露的 Brotli 远程代码执行漏洞(CVE-2026-111497)。用纯 Roc 编写的包无法越界读写,在实现的任何层面都不行,因为语言不提供允许这样做的原语。
无副作用
Roc 的类型系统推断哪些函数是纯的,哪些执行副作用。纯函数用细箭头标注,如 Bool -> Bool,而有副作用的函数用粗箭头,如 Bool => Bool。由于 roc-deflate 的公共注解只使用细箭头,编译器验证整个包由纯函数组成。
供应链安全
Roc 没有任意的外部函数接口,因此纯 Roc 包可证明无法尝试 I/O 或接触任意内存。文章认为,恶意行为者通过受损害的纯 Roc 包最多能实现的是返回错误值、崩溃或浪费系统资源。Roc 包也不支持构建脚本,这是 2026 年 8 月 Rust arrayref 包受损时使用的机制。
可移植性和确定性
Roc 纯函数被设计为在代码编译到的每个目标上,对相同参数返回相同值,而不仅仅是在同一台机器上。它们可能崩溃或无法终止,但该行为也预期跨目标具有确定性。唯一预期的跨系统差异是一个系统在执行过程中内存耗尽。
文章反向构建了这一选择:你愿意放弃有保证的内存安全、有保证的无副作用、有保证的跨目标确定性,以及没有可利用构建脚本的安全优势,来换取个位数的平均加速吗?它指出答案因用例而异,因为 Roc 自己的编译器追求最大硬件性能。
缩小差距的 Roc 特定优化
大部分收益来自通用优化技术,文章推荐 Casey Muratori 的 Performance-Aware Programming Series,作为作者付费学习来源。有四项技术是 Roc 特定的。
避免不必要的共享
Roc 最有效的优化是机会性变异,Koka 称之为"函数式但原地"(FBIP)。从性能角度看,关键规则是避免不必要的共享,以便值保持唯一,从而有资格进行原地变异。
将边界检查提升出循环
Roc 不允许程序员跳过边界检查,但编译器可以在能证明检查会通过时删除它们。边界检查在循环内最昂贵,因此在循环前添加显式检查,例如在集合长度小于或等于使用的最高索引时提前返回,可以让优化器安全地移除循环内更昂贵的检查。
在编译时构建查找表
Roc 的编译时求值可以在没有依赖运行时输入时在编译时组装查找表。因为它们的长度在编译时已知,像 Ok(entry) = lookup_table.get(5) 这样的代码变成编译器知道有效的穷举模式匹配,避免了非常数表所需的运行时边界检查。
考虑包装算术
在 Roc 中,a + b 脱糖为 a.plus(b)。对于 U64,U64.plus 包含溢出检查,而 U64.plus_wrap 不包含。当编译器能证明不会发生溢出时,两者编译为相同指令;当不能时,选择 plus_wrap 可能更快,但代价是失去更安全的默认值。
背景:Lean 4 的 lean-zip 击败了 miniz_oxide,而非 libdeflate
该基准测试的动机是早前一篇题为"为什么 Lean 比 Rust 快"的文章,该文章报告称,用 Lean 4 函数式编程语言和证明助手编写的 DEFLATE 库 lean-zip 优于 Rust 包 miniz_oxide。文章直接引用了该结果:
lean-zip击败了miniz_oxide提供的每个压缩级别,即我们能够以更高吞吐量提供相同压缩。在其最快设置下,我们快 15%,输出小 8.8%。
同一篇文章承认 lean-zip 无法匹配 libdeflate:
lean-zip当然不是这里最好的:libdeflate毫不意外地将其彻底击败(毫不意外,因为这是一个使用架构特定 SIMD 的精心调优实现,我们在 Lean 中无法触及)。
Roc 作为不是定理证明器且将运行时性能放在首位的函数式语言进入视野:其口号是"快速、友好、功能",官方性能页面认为快速编译与快速执行同样重要。Roc 尚未成熟;在撰写本文时,距离其首个编号版本 0.1.0 还有数周,其包生态系统几乎没有进行过优化工作。
Hacker News 评论者争论的内容
最实质性的挑战涉及压缩比而非吞吐量。NohatCoder 写道:
嗯,所以该程序在给定级别压缩时几乎和 libdeflate 一样快。但该帖子完全没有尝试确定给定级别的压缩效果是否与 libdeflate 中相应级别相当。在链接的帖子中,有一张图详细说明 libdeflate 在相同级别确实比原始 lean-zip 压缩得好很多。没有压缩级别数据,很难得出任何有用的结论。
另一位评论者认为 Lean 比较基线太弱。VorpalWay 写道:
Miniz_oxide 是一个故意简单的实现。将其与更新的 zlib-rs 比较会更公平,后者甚至击败了 zlib-ng(C 中最快的实现)。
VorpalWay 链接了 zlib-rs crate 和 Trifecta Tech Foundation 的文章"zlib-rs 比 C 快",作为 Rust 已经匹配或击败最快 C 实现的证据,这将改变 Roc 结果的意义。
jeffbee 质疑了 libdeflate 作为有意义性能参考的前提:
这似乎提出了一个问题:libdeflate 快吗?我对 libdeflate 的总体印象是,它产生与 zstd 相似的输出大小,但花费的时间长 40 倍。我意识到这些当然是不同的算法,但它们并没有那么不同,都属于 LZ 家族的 Huffman 系列。那么为什么 libdeflate 被视为性能基准?
jandrewrogers 为函数式语言在性能关键工作负载上往往落后的现象提供了结构性解释:
可优化性差异的很大一部分在于 C/C++ 允许进行相当极端的复制省略。这很重要,因为内存带宽往往是主要性能瓶颈。C/C++ 代码可以轻松地深入某个远程状态位并直接修改它。编译器很少反对,无论访问方法或模式多么丑陋。由程序员确保不变量成立,编译器并不总能看穿。这些省略的复制会累积,尽管通常以可维护性和安全性为代价。这与函数式语言在实践中往往在超级计算方面表现平庸有关,尽管理论上它们具有大规模并行性的优良特性。
andai 提出了文章将 Roc 描述为不成熟与声称顾问已将其用于生产多年之间的矛盾:
然后它链接到一个视频,其中一位顾问提到已将其用于生产(已经好几年了?),这让我困惑。你如何证明在客户工作中使用不成熟的技术是合理的?还是说当它出问题而你必须修复时,这是一个额外的好处?
johnny22 反对将库的下载次数作为其性能证据的框架:
说它"击败了一个被下载 X 次的 crate"除了说明它受欢迎之外,什么也没说。希望在那里看到更多细节。
ReDress 认为在性能关键系统中个位数差距是不可接受的:
我还想指出,性能在许多情况下是最终关注点。如果你有一个服务器每分钟对某个方法进行 1000 万次请求,那么你无法承受任何性能妥协。任何妥协都不行。Rust 永远不会在高性能计算方面取代 C/C++。
gobdovan 询问 Roc 特定的优化是否可以暴露为编译器检查的 pass 库,并报告了在 JavaScript 中的类似经验:在 V8 中通过足够努力可以接近优化 C 代码的约 10% 以内,除了 SIMD,gobdovan 说 V8 似乎连明显的情况都不优化。
其他评论提出了相关观点。WalterBright 指出 D 有编译器检查的 pure 函数注解。cubefox 指出 Haskell 需要仔细的尾递归才能高效使用栈,不像有循环的语言。giovannibonetti 推测 Roc 最终可能会获得高级 SIMD 抽象,并指向数据并行函数式语言 Futhark。amelius 建议长期理想是编写 FP 代码,同时 AI 使用保持正确性的变换将其翻译为更快的代码。deadbabe 询问强大的 LLM 是否消除了使用 C 以上任何语言的理由。michaelteter 喜欢 Roc 将函数式编译与 Ruby 般的易用性相结合。jdw64 询问哪些知名程序是用 Roc 构建的,以及其语法有多难。
下一步工作方向
Feldman 表示他相信 roc-deflate 中容易实现的目标已被摘取,但在 roc-deflate(尤其是在 x64 CPU 上)和 Roc 的编译器优化管线中仍看到剩余机会。他发布了用于在 Linux 上设置和运行基准测试套件的脚本,指出该脚本固定了 Roc 编译器修订版和 roc-deflate 版本,并警告该脚本由 Claude Opus 5.5 生成,尚未经过安全审查,因此应在沙箱外自行承担风险运行。
就 Feldman 而言,他表示将专注于即将发布的 Roc 0.1.0 版本,他将其描述为八年磨一剑。
Sources
相关
- Dispatch
- Dispatch
- Dispatch
- Dispatch
- Dispatch