【问题标题】:Why is Ruby irb iteration so slow?为什么 Ruby irb 迭代这么慢?
【发布时间】:2015-08-09 07:32:12
【问题描述】:

我在 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,测试 8:2.5893 :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,2.570159,测试18:2.37144: :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(毫秒),0.0468(秒)

Ruby 平均为 2483 毫秒,而 JavaScript 为 46.8 毫秒。

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

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

【问题讨论】:

  • ruby 代码和 javascript 代码没有做同样的事情,你是从 ruby​​ 中产生的(这就是它更慢的原因),你没有将回调函数传递给 javascript 代码(这是为什么它更快),所以你在这里比较苹果和橘子
  • @bjhaid 我只在 average_test 方法中屈服,而不是在 test 方法中。测试方法是对一千万次迭代进行计时的地方。我什至使用了一个没有块的 for..in 循环。
  • 如果我只比较 test 方法,Chrome 在我的机器上运行速度非常快,只有 18 毫秒,但 Firefox 大约需要 1300 毫秒(两者都在浏览器控制台中运行)。 Ruby 大约需要 720 毫秒。可能是 Chrome 的 JIT 编译器在这种类型的循环中做得很好。
  • @Genos 您正在两种解决方案中的任何一种中调用测试
  • 我从 Ruby 获得 0.47767336060000004s,从 Chrome 获得 15.45ms,从 Firefox 获得 1217.6ms。在这种情况下,Chrome 似乎确实在发挥作用。你也在用windows吗?

标签: javascript ruby unit-testing testing


【解决方案1】:

我用几个不同的 Ruby 实现尝试了你的基准测试,得到的结果大相径庭。这似乎证实了我的怀疑,即您的基准没有衡量您认为它的作用。正如我在上面的评论中提到的:在编写基准测试时,您应该始终阅读生成的本机机器代码,以验证它实际上衡量了您认为它所做的事情。

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

ruby 2.3.0dev (2015-08-08 主干 51510) [x86_64-darwin14]

这是 YARV 当前的 snapshop:

测试0:0.720945
测试一: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

如您所见,各次运行的性能非常一致,并且似乎与 cmets 中发布的其他结果一致。

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.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,而在前两次运行期间,它比 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
测试一: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

再次,编译器似乎在运行 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 的稍新快照:

测试0:0.815000
测试一: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 位服务器 VM 25.40-b25-internal-graal-0.7 on 1.8.0-internal-b128 +jit [darwin -x86_64]

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

测试 0:6.226000
测试一:5.696000
测试二:1.836000
测试 3:0.057000
测试 4:0.111000
测试 5:0.103000
测试 6:0.082000
测试 7:0.146000
测试 8:0.089000
测试 9:0.077000
测试 10:0.076000
测试 11:0.082000
测试 12:0.072000
测试 13:0.104000
测试 14:0.124000
测试 15:0.084000
测试 16:0.080000
测试 17:0.118000
测试 18:0.087000
测试 19:0.070000
平均:0.766000

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 中执行寄存器内原始整数加法操作。尽可能快。

YARV 仅在您对 Fixnum 进行猴子补丁时回退到将其视为方法调用。

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 之间的区别在于,在 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);

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

结果是:

  • 红宝石
    • YARV:223.2498 毫秒
    • JRuby:358.45 毫秒
    • 鲁比尼乌斯:477.49485 毫秒
    • JRuby+Truffle+Graal:26.4 毫秒
  • JavaScript
    • 硝基:3827.3ms
    • V8:6839 毫秒

说实话,我有点困惑。现在,Nitro 领先于 V8,所有 Ruby 实现都比 JavaScript 快 10 倍,JRuby+Truffle+Graal 再次比 Ruby 的其余部分快 10 倍,因此比 JavaScript 快 100 倍。

我猜这个真正告诉我们的是基准是没有意义的:-D

【讨论】:

  • 启发!这是我在其他地方永远无法获得或找到的信息。谢谢!
猜你喜欢
  • 1970-01-01
  • 2021-04-29
  • 2018-07-22
  • 2018-01-20
  • 2011-05-07
  • 2010-10-29
  • 1970-01-01
  • 1970-01-01
  • 2013-08-11
相关资源
最近更新 更多