我用了几种不同的Ruby实现来测试你的基准测试,结果差异很大。这似乎证实了我的怀疑,即你的基准测试并没有按照你想象中的那样进行衡量。正如我在上面的评论中提到的那样:编写基准测试时,您应该始终阅读生成的本机机器代码,并验证它实际上是否测量了您认为它测量的内容。
例如,在YARV基准测试套件中有一个基准测试旨在衡量消息分派性能,但是在Rubinius上,消息分派完全被优化掉了,因此实际执行的唯一操作就是递增基准测试循环的计数器变量。基本上,它只告诉您CPU的频率,仅此而已。
以下是Ruby 2.3.0dev(2015-08-08 trunk 51510)[x86_64-darwin14]的当前快照:
这里是当前的YARV快照:
测试0:0.720945
测试1:0.733733
测试2:0.722778
测试3:0.734074
测试4:0.774355
测试5:0.773379
测试6:0.751547
测试7:0.708566
测试8:0.724959
测试9:0.730899
测试10:0.725978
测试11:0.712902
测试12:0.747069
测试13:0.737792
测试14:0.736885
测试15:0.751422
测试16:0.718943
测试17:0.760094
测试18:0.746343
测试19:0.764731
平均值:0.738870
如您所见,性能在多个运行中非常一致,并且似乎与评论中发布的其他结果相符。
以下是Rubinius 2.5.8(2.1.0 bef51ae3 2015-08-09 3.5.1 JI)[x86_64-darwin14.4.0]的当前版本:
这里是Rubinius的当前版本:
测试 0: 1.159465
测试 1: 1.063721
测试 2: 0.516513
测试 3: 0.515016
测试 4: 0.553987
测试 5: 0.544286
测试 6: 0.567737
测试 7: 0.563350
测试 8: 0.517581
测试 9: 0.501865
测试 10: 0.503399
测试 11: 0.512046
测试 12: 0.487296
测试 13: 0.533193
测试 14: 0.533217
测试 15: 0.511648
测试 16: 0.535847
测试 17: 0.490049
测试 18: 0.539681
测试 19: 0.551324
平均值: 0.585061
可以看到,在第二次运行期间编译器启动,此后速度变为两倍,比YARV快得多,而在前两次运行期间,它明显比YARV慢。
jruby 9.0.0.0-SNAPSHOT (2.2.2) 2015-07-23 89c1348 Java HotSpot(TM) 64-Bit Server VM 25.5-b02 on 1.8.0_05-b13 +jit [darwin-x86_64]
这是在略微陈旧的HotSpot发行版(几个月)上运行的JRuby的当前快照:
测试 0: 1.169000
测试 1: 0.805000
测试 2: 0.772000
测试 3: 0.755000
测试 4: 0.777000
测试 5: 0.749000
测试 6: 0.751000
测试 7: 0.694000
测试 8: 0.696000
测试 9: 0.708000
测试 10: 0.691000
测试 11: 0.745000
测试 12: 0.752000
测试 13: 0.755000
测试 14: 0.707000
测试 15: 0.744000
测试 16: 0.674000
测试 17: 0.710000
测试 18: 0.733000
测试 19: 0.706000
平均值: 0.754650
同样的,在第一次和第二次运行之间编译器似乎启动了,之后它表现与YARV相当。
jruby 9.0.1.0-SNAPSHOT (2.2.2) 2015-08-09 2939c73 OpenJDK 64-Bit Server VM 25.40-b25-internal-graal-0.7 on 1.8.0-internal-b128 +jit [darwin-x86_64]
这是JRuby的较新版本,运行在未来版本的HotSpot上:
测试 0: 0.815000
测试 1: 0.693000
测试 2: 0.634000
测试 3: 0.615000
测试 4: 0.599000
测试 5: 0.616000
测试 6: 0.623000
测试 7: 0.611000
测试 8: 0.604000
测试 9: 0.598000
测试 10: 0.628000
测试 11: 0.627000
测试 12: 0.601000
测试 13: 0.646000
测试 14: 0.675000
测试 15: 0.611000
测试 16: 0.684000
测试 17: 0.689000
测试 18: 0.626000
测试 19: 0.639000
平均值: 0.641700
同样地,在前两次运行时速度会变快,之后会趋于稳定,在YARV和其他JRuby之间略微快一些,比Rubinius稍微慢一些。
jruby 9.0.1.0-SNAPSHOT (2.2.2) 2015-08-09 2939c73 OpenJDK 64-Bit Server VM 25.40-b25-internal-graal-0.7 on 1.8.0-internal-b128 +jit [darwin-x86_64]
这是我的最爱:启用Truffle运行在Graal-enabled JVM上的JRuby+Truffle:
测试0: 6.226秒
测试1: 5.696秒
测试2: 1.836秒
测试3: 0.057秒
测试4: 0.111秒
测试5: 0.103秒
测试6: 0.082秒
测试7: 0.146秒
测试8: 0.089秒
测试9: 0.077秒
测试10: 0.076秒
测试11: 0.082秒
测试12: 0.072秒
测试13: 0.104秒
测试14: 0.124秒
测试15: 0.084秒
测试16: 0.080秒
测试17: 0.118秒
测试18: 0.087秒
测试19: 0.070秒
平均值: 0.766秒
Truffle似乎需要显著的启动时间,前三次运行非常慢,但之后速度显著加快,使其他技术相形见绌,速度提高了5-10倍。
注意:这并不是100%公平的,因为JRuby+Truffle尚未支持完整的Ruby语言。
还要注意:这表明简单地对所有运行进行平均是非常误导性的,因为JRuby + Truffle的稳态性能实际上比YARV和JRuby快7倍。最慢的运行(JRuby + Truffle的第1个运行)和最快的运行(JRuby + Truffle的第20个运行)之间的差异高达100倍。
注意3:注意到JRuby的所有数字都以000
结尾吗?那是因为JRuby不能通过JVM轻松访问底层操作系统的微秒计时器,因此必须满足于毫秒。在这个特定的基准测试中并不太重要,但对于更快的基准测试来说,它可能会显著地扭曲结果。这只是在设计基准测试时必须考虑的另一件事。
为什么会有这么大的差异?是因为Ruby的运算符是方法调用,而方法调用很慢之类的原因吗?
我认为不是。在YARV上,Fixnum#+
甚至不是一个方法调用,它被优化为静态内置运算符。它本质上在CPU中执行寄存器中的原始整数加法操作。速度非常快。
只有当您猴子补丁Fixnum
时,YARV才回退成将其视为方法调用。
Rubinius可能可以优化掉方法调用,尽管我没有检查过。
我感觉我做错了什么。
也许你的基准测试并没有测量你所认为的东西。特别是,在使用拥有复杂优化编译器的实现中,迭代基准测试的迭代部分可能会被优化掉。
实际上,我注意到你的JavaScript和Ruby基准测试之间存在显著差异:在JavaScript中,你正在使用原始的`for`循环,而在Ruby中,你正在使用`Range#each` (`for … in`只需转换为`each`)。如果我将Ruby和JavaScript基准测试都切换到相同的`while`循环,我得到的结果是:对于Ruby版本,YARV为223ms,Rubinius为56ms,JRuby为28ms,JRuby+Truffle为33ms。对于JS版本,Squirrelfish Extreme / Nitro(Safari)为30ms,V8/Crankshaft(Chrome)为16ms。
换句话说,如果你测量相同的东西,它们最终会变得同样快;-)(好吧,除了YARV外,它总体来说就比较慢。)
因此,事实证明Ruby和JavaScript之间的区别在于,在JS中,你并没有迭代任何东西,你只是增加了一个数字,而在Ruby中,你实际上正在迭代一个数据结构(即一个`Range`)。删除Ruby中的迭代,它就像JavaScript一样快了。
我创建了两个基准测试脚本,现在希望它们大致测量相同的事情:
#!/usr/bin/env ruby
ITERATIONS = 10_000_000
TESTS = 20
WARMUP = 3
TOTALRUNS = TESTS + WARMUP
RESULTS = []
run = -1
while (run += 1) < TOTALRUNS
i = -1
starttime = Time.now
while (i += 1) < ITERATIONS do end
endtime = Time.now
RESULTS[run] = (endtime - starttime) * 1000
end
puts RESULTS.drop(WARMUP).reduce(:+) / TESTS
"use strict";
const ITERATIONS = 10000000;
const TESTS = 20;
const WARMUP = 3;
const TOTALRUNS = TESTS + WARMUP;
const RESULTS = [];
let run = -1;
while (++run < TOTALRUNS) {
let i = -1;
const STARTTIME = Date.now();
while (++i < ITERATIONS);
const ENDTIME = Date.now();
RESULTS[run] = ENDTIME - STARTTIME;
}
alert(RESULTS.slice(WARMUP).reduce((acc, el) => acc + el) / TESTS);
您会注意到我增加了迭代次数,将测试运行次数翻倍,并引入了一些不计入结果计算的热身运行。我还尽量使这两个代码片段尽可能相似。(注意:您可能需要删除一些ES6语法才能使其在浏览器上运行。例如,我的版本的Safari不喜欢箭头函数字面量。)
结果如下:
Ruby
- YARV: 223.2498ms
- JRuby: 358.45ms
- Rubinius: 477.49485ms
- JRuby+Truffle+Graal: 26.4ms
JavaScript
- Nitro: 3827.3ms
- V8: 6839ms
说实话,我有点困惑。现在Nitro领先于V8,所有Ruby实现都比JavaScript快10倍,JRuby+Truffle+Graal再次比其他Ruby要快10倍,从而使其比JavaScript快100倍。
我想这个真正告诉我们的是,这个基准测试毫无意义 :-D
test
方法,Chrome在我的机器上非常快,只需18毫秒,但Firefox需要大约1300毫秒(两者都在浏览器控制台中运行)。Ruby大约需要720毫秒。可能是Chrome的JIT编译器在这种类型的循环中表现出色。 - Bruno E.