为什么Ruby irb迭代这么慢?

时间:2015-08-09 07:32:12

标签: javascript ruby unit-testing testing

我在irb中使用Ruby的Benchmark类,我注意到Ruby在迭代时速度明显变慢。

我在没有使用Benchmark或Profiler__类的情况下做了一个简单的测试(我想也许它正在减慢速度)。

def average_test
    total_time = 0
    time = 0
    TESTS.times do |count|
        time = test
        total_time = total_time + time
        yield count, time
    end
    average = total_time / TESTS
    yield 'average', average
end
def test
    x = 0
    start_time = Time.now
    for i in 1..ITERATIONS
        x = x + 1
    end
    end_time = Time.now
    time = end_time - start_time
end
ITERATIONS = 10_000_000
TESTS = 20
# create results file
results = File.new('results.txt', 'w')
# start test
average_test {|count, time| results.print "Test #{count}: #{time}"}
results.close

以下是在irb中运行后的结果。 (在几秒钟内,抱歉)

测试0:2.390647,测试1:2.343761,测试2:2.312554,测试3:2.566792,测试4:2.665193,测试5:2.537908,测试6:2.643086,测试7:2.534492,测试8:2.589034,测试9 :2.390633,测试10:2.539533,测试11:2.385508,测试12:2.49659,测试13:2.498958,测试14:2.527309,测试15:2.462983,测试16:2.504546,测试17:2.570159,测试18:2.371447,测试19 :2.330072,

测试平均值:2.48306025(s),2483(ms)

我也在JavaScript中进行了相同的测试,只是为了比较速度。

function test() {
    var start = Date.now();
    var x = 0;
    for (var i = 0; i < ITERATIONS; i++) {
        x = x + 1;
    }
    var end = Date.now();
    var dt = end - start;
    return dt;
}
function averageTest() {
    var total = 0;
    for (var i = 0; i < TESTS; i++) {
        var time = test();
        total = total + time;
        console.log('Test ' + i + ': ', time);
    }
    var avg = total / TESTS;
    console.log('Average: ', avg);
    return avg;
}
var ITERATIONS = 10000000;
var TESTS = 20;
// start test
var avgTime = averageTest(); // results

以下是Chrome中运行的JavaScript代码的结果。 (以毫秒为单位)

测试0:41,测试1:44,测试2:41,测试3:48,测试4:46,测试5:48,测试6:49,测试7:47,测试8:46,测试9 :50,测试10:41,测试11:41,测试12:47, 测试13:54,测试14:55,测试15:57,测试16:35,测试17:50,测试18:47, 测试19:49,

平均值:46.8(ms),0.0468(s)

Ruby的平均值为2483 ms,而JavaScript的平均值为46.8 ms。

为什么会有这么大的差异?是因为Ruby的运算符是方法调用,方法调用是慢还是什么?

我觉得我做错了什么。感谢。

1 个答案:

答案 0 :(得分:4)

我尝试了几个不同的Ruby实现的基准测试,我得到了截然不同的结果。这似乎证实了我怀疑你的基准测量不是衡量你的想法。正如我在上面的评论中提到的:在编写基准时,你应该总是读取生成的本机代码,以验证实际上测量你认为它的作用。

例如,YARV基准测试套件中有一个基准测试用于测量消息调度性能,但是,在Rubinius上,消息调度完全被优化掉了,所以实际执行的唯一事情就是递增计数器变量用于基准循环。从本质上讲,它告诉你CPU的频率,仅此而已。

ruby​​ 2.3.0dev(2015-08-08 trunk 51510)[x86_64-darwin14]

这是YARV的现有快餐店:

Test  0: 0.720945
Test  1: 0.733733
Test  2: 0.722778
Test  3: 0.734074
Test  4: 0.774355
Test  5: 0.773379
Test  6: 0.751547
Test  7: 0.708566
Test  8: 0.724959
Test  9: 0.730899
Test 10: 0.725978
Test 11: 0.712902
Test 12: 0.747069
Test 13: 0.737792
Test 14: 0.736885
Test 15: 0.751422
Test 16: 0.718943
Test 17: 0.760094
Test 18: 0.746343
Test 19: 0.764731
Average: 0.738870

正如您所看到的,运行中的性能非常一致,并且似乎与评论中发布的其他结果一致。

rubinius 2.5.8(2.1.0 bef51ae3 2015-08-09 3.5.1 JI)[x86_64-darwin14.4.0]

这是Rubinius的当前版本:

Test  0: 1.159465
Test  1: 1.063721
Test  2: 0.516513
Test  3: 0.515016
Test  4: 0.553987
Test  5: 0.544286
Test  6: 0.567737
Test  7: 0.563350
Test  8: 0.517581
Test  9: 0.501865
Test 10: 0.503399
Test 11: 0.512046
Test 12: 0.487296
Test 13: 0.533193
Test 14: 0.533217
Test 15: 0.511648
Test 16: 0.535847
Test 17: 0.490049
Test 18: 0.539681
Test 19: 0.551324
Average: 0.585061

正如您所看到的,编译器会在第二次运行期间启动,之后它会快两倍,明显快于YARV,而在前两次运行中,它比YARV慢得多。

jruby 9.0.0.0-SNAPSHOT(2.2.2)2015-07-23 89c1348 Java HotSpot(TM)64位服务器VM 25.5-b02 on 1.8.0_05-b13 + jit [darwin-x86_64] < /强>

这是JRuby的一个当前快照,它运行在一个稍微旧的版本(几个月)的HotSpot上:

Test  0: 1.169000
Test  1: 0.805000
Test  2: 0.772000
Test  3: 0.755000
Test  4: 0.777000
Test  5: 0.749000
Test  6: 0.751000
Test  7: 0.694000
Test  8: 0.696000
Test  9: 0.708000
Test 10: 0.691000
Test 11: 0.745000
Test 12: 0.752000
Test 13: 0.755000
Test 14: 0.707000
Test 15: 0.744000
Test 16: 0.674000
Test 17: 0.710000
Test 18: 0.733000
Test 19: 0.706000
Average: 0.754650

同样,编译器似乎在运行1和2之间的某个位置启动,之后它与YARV表现相当。

jruby 9.0.1.0-SNAPSHOT(2.2.2)2015-08-09 2939c73 OpenJDK 64位服务器VM 25.40-b25-internal-graal-0.7 on 1.8.0-internal-b128 + jit [darwin -x86_64]

这是在未来版本的HotSpot上运行的JRuby的一个稍微更新的快照:

Test  0: 0.815000
Test  1: 0.693000
Test  2: 0.634000
Test  3: 0.615000
Test  4: 0.599000
Test  5: 0.616000
Test  6: 0.623000
Test  7: 0.611000
Test  8: 0.604000
Test  9: 0.598000
Test 10: 0.628000
Test 11: 0.627000
Test 12: 0.601000
Test 13: 0.646000
Test 14: 0.675000
Test 15: 0.611000
Test 16: 0.684000
Test 17: 0.689000
Test 18: 0.626000
Test 19: 0.639000
Average: 0.641700

同样,我们看到它在前两次运行中的模式变得更快,之后它在比YARV和其他JRuby稍微快一点的地方之间稳定,并且比Rubinius略慢。

jruby 9.0.1.0-SNAPSHOT(2.2.2)2015-08-09 2939c73 OpenJDK 64位服务器VM 25.40-b25-internal-graal-0.7 on 1.8.0-internal-b128 + jit [darwin -x86_64]

这是我的最爱:JRuby + Truffle启用了Truffle并在支持Graal的JVM上运行:

Test  0:  6.226000
Test  1:  5.696000
Test  2:  1.836000
Test  3:  0.057000
Test  4:  0.111000
Test  5:  0.103000
Test  6:  0.082000
Test  7:  0.146000
Test  8:  0.089000
Test  9:  0.077000
Test 10:  0.076000
Test 11:  0.082000
Test 12:  0.072000
Test 13:  0.104000
Test 14:  0.124000
Test 15:  0.084000
Test 16:  0.080000
Test 17:  0.118000
Test 18:  0.087000
Test 19:  0.070000
Average:  0.766000

Truffle似乎需要显着量的加速时间,前三次运行非常慢,但随后它显着加快速度,留下其他所有内容在灰尘中的因子为5-10。

注意:这不是100%公平,因为JRuby + Truffle还不支持完整的Ruby语言。

另请注意:这表明,仅仅取平均值就非常误导,因为JRuby + Truffle与YARV和JRuby的平均值相同,但实际上稳态性能提高了7倍。最慢的运行(JRuby + Truffle的第1次运行)和最快的运行(JRuby + Truffle的第20次运行)之间的差异是100x。

注意#3:注意JRuby数字如何以000结束?这是因为JRuby无法通过JVM轻松访问底层操作系统的微秒计时器,因此必须满足毫秒。在这个特定的基准测试中,它并不重要太多,但是对于更快的基准测试,它可能会显着地扭曲结果。这只是设计基准测试时必须考虑的另一件事。

  

为什么会有这么大的差异?是因为Ruby的运算符是方法调用,方法调用很慢还是什么?

我不这么认为。在YARV上,Fixnum#+甚至不是方法调用,它针对静态内置运算符进行了优化。它实质上在CPU中执行寄存器内原始整数添加操作。尽可能快。

当您修补Fixnum时,YARV只会将其视为方法调用。

Rubinius可能会优化方法调用,虽然我没有检查。

  

我觉得我做错了什么。

可能您的基准测量不能衡量您的想法。特别是,我相信在使用复杂优化编译器的实现中,迭代基准测试的迭代部分可能会被优化掉。

实际上,我注意到你的JavaScript和Ruby基准测试之间存在显着差异:在JavaScript中,你使用原始的for循环,在Ruby中,你正在使用Range#eachfor … 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之间的区别在于,你在中没有迭代任何东西,你只是递增一个数字,而在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

&#13;
&#13;
"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);
&#13;
&#13;
&#13;

您会注意到我增加了迭代次数,我将测试运行次数增加了一倍,并且我引入了许多预算运行,这些运行未包含在计算结果中。我还尝试使两个片段尽可能相似。 (注意:您可能必须删除一些ES6主机才能在浏览器上运行。例如,我的Safari版本不喜欢胖箭头函数文字。)

结果是:

  • 红宝石
    • 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