MRI Ruby和jRuby的性能差异

5
在为回答关于最快地连接数组的这个问题做一些基准测试时,我惊讶地发现在jRuby中进行相同的基准测试时,速度要慢得多。这是否意味着关于jRuby比MRI Ruby更快的古老说法已经不存在了?还是这与jRuby中如何处理数组有关?
这里是基准测试和MRI Ruby 2.3.0和jRuby 9.1.2.0的结果。两者都在64位Windows 7上运行,4个处理器的占用率为50-60%,内存使用量约为5.5GB。必须使用参数-J-Xmx1500M启动jRuby以提供足够的堆空间。由于堆栈层数太深而不得不删除使用push的测试,并且删除了最慢的方法以使测试时间不要太长。所使用的Java运行时版本为1.7.0_21。
require 'Benchmark'
N = 100

class Array
  def concat_all 
    self.reduce([], :+)
  end
end

# small arrays
a = (1..10).to_a
b = (11..20).to_a
c = (21..30).to_a

Benchmark.bm do |r|
  r.report('plus       ')  { N.times { a + b + c }}
  r.report('concat     ') { N.times { [].concat(a).concat(b).concat(c) }}
  r.report('splash     ') { N.times {[*a, *b, *c]} }
  r.report('concat_all ')  { N.times { [a, b, c].concat_all }}
  r.report('flat_map   ') { N.times {[a, b, c].flat_map(&:itself)} }
end

#large arrays
a = (1..10_000_000).to_a
b = (10_000_001..20_000_000).to_a
c = (20_000_001..30_000_000).to_a

Benchmark.bm do |r|
  r.report('plus       ')  { N.times { a + b + c }}
  r.report('concat     ') { N.times { [].concat(a).concat(b).concat(c) }}
  r.report('splash     ') { N.times {[*a, *b, *c]} }
  r.report('concat_all ')  { N.times { [a, b, c].concat_all }}
  r.report('flat_map   ') { N.times {[a, b, c].flat_map(&:itself)} }
end

这个问题不涉及使用的不同方法,请参阅原始问题。在这两种情况下,MRI速度快了7倍!有人可以解释一下原因吗?我也很好奇其他实现的情况,比如RBX(Rubinius)。
C:\Users\...>d:\jruby\bin\jruby -J-Xmx1500M concat3.rb
       user     system      total        real
plus         0.000000   0.000000   0.000000 (  0.000946)
concat       0.000000   0.000000   0.000000 (  0.001436)
splash       0.000000   0.000000   0.000000 (  0.001456)
concat_all   0.000000   0.000000   0.000000 (  0.002177)
flat_map  0.010000   0.000000   0.010000 (  0.003179)
       user     system      total        real
plus       140.166000   0.000000 140.166000 (140.158687)
concat     143.475000   0.000000 143.475000 (143.473786)
splash     139.408000   0.000000 139.408000 (139.406671)
concat_all 144.475000   0.000000 144.475000 (144.474436)
flat_map143.519000   0.000000 143.519000 (143.517636)

C:\Users\...>ruby concat3.rb
       user     system      total        real
plus         0.000000   0.000000   0.000000 (  0.000074)
concat       0.000000   0.000000   0.000000 (  0.000065)
splash       0.000000   0.000000   0.000000 (  0.000098)
concat_all   0.000000   0.000000   0.000000 (  0.000141)
flat_map     0.000000   0.000000   0.000000 (  0.000122)
       user     system      total        real
plus        15.226000   6.723000  21.949000 ( 21.958854)
concat      11.700000   9.142000  20.842000 ( 20.928087)
splash      21.247000  12.589000  33.836000 ( 33.933170)
concat_all  14.508000   8.315000  22.823000 ( 22.871641)
flat_map    11.170000   8.923000  20.093000 ( 20.170945)

我找到了罪魁祸首:-J-Xmx1500M。一开始我没有使用这个选项,所以Java使用了默认的最大堆大小(在我的系统上为4096M),并且工作得很好。如果我提供该选项,将值降低到1500M,我会得到非常缓慢的结果。 - Stefan
...至少20120个数据点。(并且您需要丢弃前20020个测量值。)最好再多一些,因为编译后还有一个稳定期。 - Jörg W Mittag
@JörgWMittag和Stefan,请将其放在答案中,以便我们可以更好地在评论中讨论。 - peter
@peter,你能否在你的问题中加入rbx吗?最近没有比较这三种Ruby的基准答案。 - nurettin
@nurettin 测试了一下自己,这是测试我的 Docker 技能的机会,看看我自己的答案,希望在真正的 Linux 上结果会更好。 - peter
显示剩余4条评论
2个回答

4

通常情况下(如评论中所提到的)JRuby/JVM需要热身。

一般来说,bmbm 是一个不错的选择,但是 TIMES=1000 应该增加(至少对于小数组情况),此外 1.5G 可能不能达到 JRuby 的最佳性能(从 -Xmx2g 到 -Xmx3g 发现数字有显著变化)。这是结果:

ruby 2.3.1p112 (2016-04-26 revision 54768) [x86_64-linux]

$ ruby concat3.rb
Rehearsal -----------------------------------------------
plus          0.000000   0.000000   0.000000 (  0.000076)
concat        0.000000   0.000000   0.000000 (  0.000070)
splash        0.000000   0.000000   0.000000 (  0.000099)
concat_all    0.000000   0.000000   0.000000 (  0.000136)
flat_map      0.000000   0.000000   0.000000 (  0.000138)
-------------------------------------- total: 0.000000sec

                  user     system      total        real
plus          0.000000   0.000000   0.000000 (  0.000051)
concat        0.000000   0.000000   0.000000 (  0.000059)
splash        0.000000   0.000000   0.000000 (  0.000083)
concat_all    0.000000   0.000000   0.000000 (  0.000120)
flat_map      0.000000   0.000000   0.000000 (  0.000173)
Rehearsal -----------------------------------------------
plus         43.040000   3.320000  46.360000 ( 46.351004)
concat       15.080000   3.870000  18.950000 ( 19.228059)
splash       49.680000   4.820000  54.500000 ( 54.587707)
concat_all   51.840000   5.260000  57.100000 ( 57.114867)
flat_map     17.380000   5.340000  22.720000 ( 22.716987)
------------------------------------ total: 199.630000sec

                  user     system      total        real
plus         42.880000   3.600000  46.480000 ( 46.506013)
concat       17.230000   5.290000  22.520000 ( 22.890809)
splash       60.300000   7.480000  67.780000 ( 67.878534)
concat_all   54.910000   6.480000  61.390000 ( 61.404383)
flat_map     17.310000   5.570000  22.880000 ( 23.223789)

...

jruby 9.1.6.0 (2.3.1) 2016-11-09 0150a76 Java HotSpot(TM) 64-Bit Server VM 25.112-b15 on 1.8.0_112-b15 +jit [linux-x86_64]

$ jruby -J-Xmx3g concat3.rb
Rehearsal -----------------------------------------------
plus          0.010000   0.000000   0.010000 (  0.001445)
concat        0.000000   0.000000   0.000000 (  0.002534)
splash        0.000000   0.000000   0.000000 (  0.001791)
concat_all    0.000000   0.000000   0.000000 (  0.002513)
flat_map      0.010000   0.000000   0.010000 (  0.007088)
-------------------------------------- total: 0.020000sec

                  user     system      total        real
plus          0.010000   0.000000   0.010000 (  0.002700)
concat        0.000000   0.000000   0.000000 (  0.001085)
splash        0.000000   0.000000   0.000000 (  0.001569)
concat_all    0.000000   0.000000   0.000000 (  0.003052)
flat_map      0.000000   0.000000   0.000000 (  0.002252)
Rehearsal -----------------------------------------------
plus         32.410000   0.670000  33.080000 ( 17.385688)
concat       18.610000   0.060000  18.670000 ( 11.206419)
splash       57.770000   0.330000  58.100000 ( 25.366032)
concat_all   19.100000   0.030000  19.130000 ( 13.747319)
flat_map     16.160000   0.040000  16.200000 ( 10.534130)
------------------------------------ total: 145.180000sec

                  user     system      total        real
plus         16.060000   0.040000  16.100000 ( 11.737483)
concat       15.950000   0.030000  15.980000 ( 10.480468)
splash       47.870000   0.130000  48.000000 ( 22.668069)
concat_all   19.150000   0.030000  19.180000 ( 13.934314)
flat_map     16.850000   0.020000  16.870000 ( 10.862716)

看起来恰好相反 - MRI 2.3 的速度比 JRuby 9.1 慢了2-5倍

cat concat3.rb
require 'benchmark'
N = (ENV['TIMES'] || 100).to_i

class Array
  def concat_all
    self.reduce([], :+)
  end
end

# small arrays
a = (1..10).to_a
b = (11..20).to_a
c = (21..30).to_a

Benchmark.bmbm do |r|
  r.report('plus       ')  { N.times { a + b + c }}
  r.report('concat     ') { N.times { [].concat(a).concat(b).concat(c) }}
  r.report('splash     ') { N.times {[*a, *b, *c]} }
  r.report('concat_all ')  { N.times { [a, b, c].concat_all }}
  r.report('flat_map   ') { N.times {[a, b, c].flat_map(&:itself)} }
end

#large arrays
a = (1..10_000_000).to_a
b = (10_000_001..20_000_000).to_a
c = (20_000_001..30_000_000).to_a

Benchmark.bmbm do |r|
  r.report('plus       ')  { N.times { a + b + c }}
  r.report('concat     ') { N.times { [].concat(a).concat(b).concat(c) }}
  r.report('splash     ') { N.times {[*a, *b, *c]} }
  r.report('concat_all ')  { N.times { [a, b, c].concat_all }}
  r.report('flat_map   ') { N.times {[a, b, c].flat_map(&:itself)} }
end

请问您能否将Rubinius添加到您的基准测试中呢? :-) - nurettin

1
我从这些评论、回答和之后自己的测试中学到了以下内容:
  • 操作系统可能会有所不同,我希望能得到更多不同情况下的答案,因此这里只是猜测
  • 最快的方法取决于运行时、MRI或jRuby、32位还是64位、JRE,所以声称某种方法比另一种方法更好是困难的,在我的系统上,几乎所有情况下加法方法都是最快的,但我没有像kares那样使用Java HotSpot
  • 在64位jRuby中,您可以指定比32位高得多的堆(在我的系统上为1.5G),在64位中,我可以使用比内存更多的堆(某处出现了错误?)
  • 更高的堆加速了使用大量内存的操作,例如我使用的巨大数组
  • 使用最新的Java运行时,速度更快
  • jRuby需要预热,方法需要运行多次才能编译,因此使用.bm和.bmbm以不同的重复值来找到边界
  • 有时候MRI更快,但是通过正确的参数和预热,对于这个特定的测试,jRuby在我的系统上快3到3.5倍
最后,加上JVM的加载,使MRI更适合短暂的即席脚本,而jRuby则更适合对进程饥饿、运行时间较长且方法经常重复的情况,因此jRuby更适合于运行服务器和服务。
我看到的证实了:对于长时间或重复的进程,请自己进行基准测试。两种实现与早期版本相比在速度方面都有很大改进,不要忘记:Ruby可能是一个慢跑者,但是开发速度更快,如果你将一些额外硬件的成本与一些额外开发人员的成本进行比较...
感谢所有评论者和karen的专业知识。
编辑
出于好奇,我还在docker容器中使用Rubinius运行了测试(我在Windows上),rubinius 3.69 (2.3.1 a57071c6 2016-11-17 3.8.0) [x86_64-linux-gnu]只有concat和flat_map与MRI相当,我想知道这些方法是否是用C编写的,而其余的则是纯Ruby...
Rehearsal -----------------------------------------------
plus          0.000000   0.000000   0.000000 (  0.000742)
concat        0.000000   0.000000   0.000000 (  0.000093)
splash        0.000000   0.000000   0.000000 (  0.000619)
concat_all    0.000000   0.000000   0.000000 (  0.001357)
flat_map      0.000000   0.000000   0.000000 (  0.001536)
-------------------------------------- total: 0.000000sec

                  user     system      total        real
plus          0.000000   0.000000   0.000000 (  0.000589)
concat        0.000000   0.000000   0.000000 (  0.000084)
splash        0.000000   0.000000   0.000000 (  0.000596)
concat_all    0.000000   0.000000   0.000000 (  0.001679)
flat_map      0.000000   0.000000   0.000000 (  0.001568)
Rehearsal -----------------------------------------------
plus         68.770000  63.320000 132.090000 (265.589506)
concat       20.300000   2.810000  23.110000 ( 23.662007)
splash       79.310000  74.090000 153.400000 (305.013934)
concat_all   83.130000 100.580000 183.710000 (378.988638)
flat_map     20.680000   0.960000  21.640000 ( 21.769550)
------------------------------------ total: 513.950000sec

                  user     system      total        real
plus         65.310000  70.300000 135.610000 (273.799215)
concat       20.050000   0.610000  20.660000 ( 21.163930)
splash       79.360000  80.000000 159.360000 (316.366122)
concat_all   84.980000  99.880000 184.860000 (383.870653)
flat_map     20.940000   1.760000  22.700000 ( 22.760643)

是的,提到你使用的JVM很重要。如果只说Java,人们会认为是HotSpot。同时知道版本也总是很好的,有时(虽然很少)特定的Java版本可能会有改进。显然要使用最新的JRuby版本(9.1.2存在错误 - 如果可能,请使用更新的9.1.x)。允许比内存更大的堆不是错误 - 当然有点次优(JVM至少可以打印警告)...因为你仍然可能有swap :) - kares

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接