发布于 2026-01-06 0 阅读
0

是的,Ruby 速度很快,但是……

是的,Ruby 速度很快,但是……

John Hawthorn 写了一篇不错的文章,讨论了最近一款将Crystal 集成到 Ruby 应用中的工具。虽然 JH 的观点很重要,但他忽略了一些值得考虑的方面。我将探讨 Crystal 的实际性能和优势,并重点说明为什么这种 Ruby/Crystal 集成工具是必备的。

这也是对Hacker News 帖子下的一些评论进行结构化的呈现

tl;dr

  • JH 认为 Ruby 具有即时编译器,并且优化 Ruby 版本的代码可以极大地提高性能。
  • Crystal 代码不需要经过复杂的调整才能达到最佳状态。
  • 该比较是在 Ruby 中进行的,也就是说,将调用 Crystal 的成本也考虑在 Ruby 中。
  • 纯净水晶展现出截然不同的景象!

是的,Ruby 速度很快。

首先我想指出的是,JH 的观点是正确的:我们需要公平对待 Ruby 的 JIT 编译器--yjit,只考虑那些包含 JIT 编译器的基准测试。事实上,有了 JIT 编译器,Ruby 的性能确实非常出色。

坦白说,我非常喜欢 Ruby! Ruby 是我最喜欢的五种编程语言之一。而且,Ruby 拥有庞大的社区,许多大公司也认同这一点,所以我相信随着更多改进的加入,Ruby 的性能只会不断提升。

🔴第一点: Ruby 的 YJIT 速度很快!

JIT 和 Crystal 的实际性能

让我们比较一下 Ruby 的 YJIT、Python PyPy(另一个 JIT 编译器)和纯 Crystal(即没有集成)的执行情况。

Ruby:在我的电脑上,Ruby 的 YJIT 测试结果与文章中的数据一致。每一行都对应着文中提出的每一项优化:

> ruby --yjit fib.rb
       user     system      total        real
   3.464166   0.022979   3.487145 (  3.491493)
   1.705869   0.002169   1.708038 (  1.710117)
   0.187083   0.000318   0.187401 (  0.187578)
Enter fullscreen mode Exit fullscreen mode

Python:我的 Python 水平有限,所以我只移植了最后一个问题(一个简单的 while 循环),并用PyPy运行了它。它运行时间稍微短一些:

>  pypy fib.py
0.12447810173
Enter fullscreen mode Exit fullscreen mode

Crystal:当我们用 编译代码时--release,数字就变得无关紧要了!不仅如此,我还添加了一些额外的代码,以确保优化不会丢弃重要的代码。所以我不仅计算了斐波那契数列中的 45(为了进一步扩展,我使用了 UInt128),而且还打印了百万次运行的总和!

> crystal build --release fib.cr; ./fib
        user     system      total        real
  1134903170000000
  0.000002   0.000004   0.000006 (  0.000004)
  1134903170000000
  0.000001   0.000002   0.000003 (  0.000003)
  1134903170000000
  0.000002   0.000002   0.000004 (  0.000003)
Enter fullscreen mode Exit fullscreen mode

第二点: Pure Crystal在这个基准测试中真的非常非常快!

参考:我用于基准测试的代码列在这个gist中。

注:如前所述,Crystal 版本使用原始数值类型(UInt128)。这解释了性能差异的主要原因。

Crystal 编译可以优化你的代码

在 Crystal 程序的计时结果中,第一个程序比第二个程序多耗时几微秒。但是,如果我们交换这两个示例的运行顺序,结果却完全相同:无论是哪个示例,第一个程序都比第二个程序多耗时几微秒。

总之,对 Ruby 版本代码提出的任何修改都无法对 Crystal 版本产生影响。这并非完全是 Crystal 的问题:它使用了LLVM后端,而 LLVM 后端生成的二进制文件经过了高度优化。

坦白说,我很困惑为什么 Ruby 的 YJIT 没有很好地优化这一点。也许随着时间的推移它会改进(我测试的是 Ruby 3.3.1)。

第三点: Crystal 代码运行速度很快,即使不做任何优化。

或许是水管排水慢?

似乎并非如此。但要理解其中的原因,我们需要讨论一个重要的观点:默认情况下,集成编译 Crystal 代码时不会指定编译--release标志。这很合理:在开发过程中,您不希望编译耗时过长。以发布模式编译可以生成高效的二进制文件,但代价是编译时间显著增加。

当我使用发布模式测试crystalruby 页面README 文件中的素数计数功能时,运行 Crystal 代码所需的时间与纯 Crystal 代码的运行时间相同。为此,需要添加以下代码:

CrystalRuby.configure do |config|
  config.debug = false
end
Enter fullscreen mode Exit fullscreen mode

所以,斐波那契数列示例中的计时结果或许会和纯 Crystal 代码的计算结果一样。之所以说“或许”,是因为我偶然发现了一个问题,导致该示例的集成功能无法使用。

🔴⚫第四点:默认情况下,集成不会生成高效的 Crystal 代码。

Crystal/Ruby 集成再探

Crystal 和 Ruby 都是非常优秀的语言,各有优缺点。Crystal 的性能和低内存占用几乎无可争议,这一点还可以通过语言和编译器的基准测试进一步研究(但要理性看待基准测试结果!)。

Crystal 的优势不仅在于性能:它的类型检查器也是一大亮点,团队可能会希望将其用于应用程序中安全至关重要的部分。或者,也许某个 gem 中存在值得调用的分片……无论出于何种原因,将 Crystal 代码集成到 Ruby 中都是开发工具箱中一个极具吸引力的工具。

从 Ruby 或 Crystal 调用 C 函数是很常见的。有趣的是,还有其他方法可以连接这两种语言,它们的目标都是使用类似的语法编写优美的程序。前面提到的crystalruby gem 允许 Ruby 程序与 Crystal 交互,而 shard anyolite则允许从 Crystal 调用 Ruby 程序。

🔴⚫第五点:红宝石+水晶万岁!❤️

编辑:我收到了两次非常好的问题:我们如何确定 LLVM 没有进行过多的优化,只是用计算结果替换了对斐波那契函数的调用?毕竟,参数是固定的,它可以计算出结果的大小并直接替换。

虽然我一开始也想到了这一点,但我在文章中漏掉了它。写这篇文章的时候,我尝试添加了45 + rand(1)参数。这样可以确保参数不是一个字面数字。这确实会影响整体性能,现在只需要 1 毫秒。这仍然非常好,因为它也统计了对 ` rand!` 的调用次数。这就是为什么我没发现问题,也忘了把它添加到文章里。

然而,进一步检查 LLVM 生成的代码后,我发现了更多!它居然还优化了代码!它对函数进行了数fib(45)百万次调用,最终结果竟然达到了1134903170 rand(1)!我简直震惊了。总之,推荐使用 LLVM,也建议 Crystal 使用它!

更新 2: GitHub 用户@petr-fischer建议从命令行获取参数,以强制 LLVM 不要进行过多优化。做出此更改后,时间性能有了显著变化,尤其可以从第二个版本和第三个版本中看出差异:

        user     system      total        real
    0.034982   0.000266   0.035248 (  0.035400)
    0.034268   0.000134   0.034402 (  0.034522)
    0.023234   0.000140   0.023374 (  0.023607)
Enter fullscreen mode Exit fullscreen mode

我认为结论并没有改变:我们仍然在讨论相对于 Ruby 或 Python 版本而言的显著减少。而且正如前面提到的,我要强调的是,这其中很大一部分是使用原始类型(可以看看George Dietrich 在论坛上推荐的Ary 的这篇文章)。

一张生成的带有黑色色调的红色多面体图像

文章来源:https://dev.to/betaziliani/yes-ruby-is-fast-but-1l49