3

我有一个 Ruby 网络服务,我最近检查了使用 JRuby(9.1.17.0,OpenJDK 1.8)相对于当前使用 MRI(2.5.0)是否会提高性能。我预计可能会出现这种情况,因为性能瓶颈是为计算响应数据而执行的大量“基本算术”,而 JRuby 在计算量大的基准测试中往往优于 MRI。

然而,事实证明并非如此:我尝试了许多 JRuby/JVM 选项的组合,但“稳定状态”比 MRI 慢 2 倍。在重复请求约 100 次后达到稳定状态,此时 JVM 显然正在发挥其 JIT 魔力,因为性能相对于初始请求提高了 2.5 倍。

我想了解这是预期的还是意外的行为。所以我想知道:预计 JRuby 会比 MRI 慢的典型工作负载是什么?其中确实是“浮点数的基本算术”吗?

(MRI 和 JRuby 的性能瓶颈在同一个地方,使用适当的分析器确定。最初这篇文章说 JRuby 只慢了 20%,但我已经引入了一项优化,将 MRI 性能提高了近 2 倍,但是几乎没有改变 JRuby 的性能。我怀疑 JVM 自动执行了相同的优化,因为它基本上相当于“恒定折叠”)

4

1 回答 1

3

如果您在Integers 上进行计算,并且Integers 适合native_word_size - 1位,那么 YARV 将在Fixnums 上使用本机机器算法。如果您在Floats 上进行计算,在 64 位平台上,并且您的计算适合 62 位,YARV 将在flonums上使用本机 FPU 算法。在任何一种情况下,它都不会比这更快,除非您的操作非常简单,以至于 JVM JIT(或 JRuby 编译器)可以完全优化它们,不断折叠它们或类似的东西。

最佳点是Integer大于 63 位但小于 64 位的 s,JRuby 将其视为本机机器整数,但 YARV 将其视为本机机器整数,Float大于 62 但小于 64 位的 s 也是如此。在此范围内,JRuby 将使用本机操作,但 YARV 不会,这为 JRuby 提供了性能优势。

一般来说,YARV 在延迟方面优于 JRuby ,尤其是在启动时间方面。不过,这在很大程度上取决于所使用的 JVM 和环境。有些 JVM 旨在快速启动(例如 IBM J9,IMO 应该是默认桌面 JVM 而不是 Oracle HotSpot)或 Avian(它实际上不是 JVM,因为它只实现了 JVM 和 JRE 的子集规范,但仍然可以运行许多不使用任何未实现特性的重要程序,JRuby 就是其中之一。)此外,还有一些环境和配置,允许您保留和重用 JVM和内存中的 JRuby 实例,从而消除了大部分启动时间。

第二个大问题是 YARV C 扩展。YARV 为 C 扩展提供了一个非常开放和广泛的 API。本质上,YARV C 扩展可以访问 YARV 的几乎所有私有内部实现细节。(这显然意味着他们可以破坏和崩溃 YARV。)另一方面,JVM“C 扩展”总是需要通过安全屏障。它们只能破坏由调用它们的 Java 代码显式传递给它们的内存,它们永远不能破坏其他内存,更不用说 JVM 本身了。然而,这是以性能为代价的:从 Java 调用 C 或反之亦然通常比从 YARV 调用 C 慢,反之亦然。

YARV C 扩展甚至比这更慢,因为 JRuby 本质上必须提供一个完整的复杂模拟层,模拟 YARV 的内部数据结构、函数和内存布局,以便至少运行一些 YARV C 扩展。这只是慢。时期。

请注意,这不适用于使用 Ruby FFI API 的 C 库的 Ruby 包装器。这些不依赖于 YARV 内部,因此不需要仿真层,而且 JRuby 有一个非常快速和优化的 Ruby FFI API 实现。不过,JVM ↔ C 桥接的成本仍然适用。

这是 YARV 更快的两大方面:运行时间太短而无法利用 JVM 对长时间运行的进程进行优化的代码,以及大量使用 C 调用(尤其是YARV C 扩展)的代码。

如果您可以让您的代码在 TruffleRuby 上运行,那将是一个有趣的实验。TruffleRuby 可以做的优化确实令人惊叹(例如,使用大量动态元编程、反射和Hash查找将整个 Ruby 库折叠到一个常量中),它可以接近甚至击败手动优化的 C。此外,TruffleRuby 包含一个 C 解释器除了 Ruby 解释器,因此可以分析和优化 Ruby 代码调用 C 扩展,反之亦然,甚至执行跨语言内联,这意味着在某些基准测试中,它可以更快地执行大量使用 YARV 扩展的 Ruby 代码比 YARV!

于 2018-05-05T20:08:15.633 回答