Ruby数组concat与+的速度相比如何?

28

我进行了Ruby数组的concat()+操作的小型性能测试,结果concat()速度非常快。

然而,我不清楚为什么concat()如此快?

有人可以帮忙解释一下吗?

这是我使用的代码:

t = Time.now
ar = []
for i in 1..10000
ar = ar + [4,5]
end
puts "Time for + " + (Time.now - t).to_s 


t = Time.now
ar = []
for i in 1..10000
ar.concat([4,5])
end
puts "Time for concat " + (Time.now - t).to_s 

8
FYI :) http://www.ruby-doc.org/stdlib-1.9.3/libdoc/benchmark/rdoc/Benchmark.html - Logan Serman
1
http://www.joelonsoftware.com/articles/fog0000000319.html - Darth Egregious
6个回答

48
根据Ruby文档,两者的区别如下: Array#+: 连接 - 返回通过连接两个数组形成的第三个数组。 Array#concat: 将other_ary的元素追加到self中。
因此,每次调用+运算符都会创建一个新数组(非常昂贵),而concat仅附加新元素。

19
答案:

问题在于Ruby的底层C实现中的+运算符和concat方法。

Array#+

rb_ary_plus(VALUE x, VALUE y)
{
    VALUE z;
    long len, xlen, ylen;

    y = to_ary(y);
    xlen = RARRAY_LEN(x);
    ylen = RARRAY_LEN(y);
    len = xlen + ylen;
    z = rb_ary_new2(len);

    ary_memcpy(z, 0, xlen, RARRAY_CONST_PTR(x));
    ary_memcpy(z, xlen, ylen, RARRAY_CONST_PTR(y));
    ARY_SET_LEN(z, len);
    return z;
}
Array#concat
rb_ary_concat(VALUE x, VALUE y)
{
    rb_ary_modify_check(x);
    y = to_ary(y);
    if (RARRAY_LEN(y) > 0) {
        rb_ary_splice(x, RARRAY_LEN(x), 0, y);
    }
    return x;
}

如您所见,+操作符正在复制每个数组的内存,然后创建并返回一个包含两个数组内容的第三个数组。而concat方法只是将新数组插入到原始数组中。


+=是什么?从技术上讲,它和#concat一样吗? - Dalibor Filus
2
在 Ruby 中,您可以以几种方式编写操作数表达式。这里最相关的是 x = x + y,它等同于 x += y。除非一个类专门重写加号运算符以委托给 concat,否则 += 不会像 concat 一样完全执行,因为它使用的是 rb_ary_plus 而不是 rb_ary_concat - Chris Cashwell

8
如果您要运行基准测试,请利用预构建的工具,并将测试缩小到最小限度,以测试您想了解的内容。从Fruity开始,它为基准测试提供了很多智能化支持。
require 'fruity'

compare do
  plus { [] + [4, 5] }
  concat { [].concat([4, 5]) }
end
# >> Running each test 32768 times. Test will take about 1 second.
# >> plus is similar to concat

当事物足够接近而不必担心时,Fruity会告诉我们它们是“相似的”。
此时,Ruby内置的Benchmark类可以帮助:
require 'benchmark'

N = 10_000_000
3.times do
  Benchmark.bm do |b|
    b.report('plus')  { N.times { [] + [4, 5] }}
    b.report('concat') { N.times { [].concat([4,5]) }}
  end
end
# >>        user     system      total        real
# >> plus  1.610000   0.000000   1.610000 (  1.604636)
# >> concat  1.660000   0.000000   1.660000 (  1.668227)
# >>        user     system      total        real
# >> plus  1.600000   0.000000   1.600000 (  1.598551)
# >> concat  1.690000   0.000000   1.690000 (  1.682336)
# >>        user     system      total        real
# >> plus  1.590000   0.000000   1.590000 (  1.593757)
# >> concat  1.680000   0.000000   1.680000 (  1.684128)

请注意不同的时间。运行一次测试可能会导致误导性的结果,因此要运行多次。另外,请确保您的循环产生的时间不会被由进程启动引起的后台噪音所掩盖。

仅包括选择输入的实际性能时间相对来说是具有误导性的,因为只有很小的n值才会产生非常“温和”的差异。 - user2864740
(实际上,原始测试通过“意外副作用”更好地展示了性能差异。) - user2864740

3

正如其他答案中所指出的,OP的问题是比较执行不同操作的两个运算符。其中一个是concat,它对原始数组具有破坏性(改变),而另一个是+,它是非破坏性的(纯函数式,不改变)。

我来这里是为了寻找一个更可比较的测试,并没有意识到concat是有破坏性的。如果其他人想要比较两个纯函数式、非破坏性操作,这里提供一个数组相加(array1 + array2)和数组扩展([*array1, *array2])的基准测试。据我所知,两者都会创建3个数组:2个输入数组,1个新的结果数组。

提示:+获胜。

代码

# a1 is a function producing a random array to avoid caching
a1 = ->(){ [rand(10)] }
a2 = [1,2,3]
n = 10_000_000
Benchmark.bm do |b|
  b.report('expand'){ n.times{ [*a1[], *a2] } }
  b.report('add'){ n.times{ a1[]+a2 } }
end

结果

user     system      total        real
expand  9.970000   0.170000  10.140000 ( 10.151718)
add  7.760000   0.020000   7.780000 (  7.792146)

2

我使用了两个版本的Ruby进行基准测试。结果显示,concat比plus(+)更快。

require 'benchmark'

N = 10_000_000
5.times do
  Benchmark.bm do |b|
    b.report('concat') { N.times { [].concat([4,5]) }}
    b.report('plus')  { N.times { [] + [4, 5] }}
  end
end

ruby-2.5.3

       user     system      total        real
concat  1.347328   0.001125   1.348453 (  1.349277)
plus  1.405046   0.000110   1.405156 (  1.405682)
       user     system      total        real
concat  1.263601   0.012012   1.275613 (  1.276105)
plus  1.336407   0.000051   1.336458 (  1.336951)
       user     system      total        real
concat  1.264517   0.019985   1.284502 (  1.285004)
plus  1.329239   0.000002   1.329241 (  1.329733)
       user     system      total        real
concat  1.347648   0.004012   1.351660 (  1.352149)
plus  1.821616   0.000034   1.821650 (  1.822307)
       user     system      total        real
concat  1.256387   0.000000   1.256387 (  1.256828)
plus  1.269306   0.007997   1.277303 (  1.277754)

ruby-2.7.1

       user     system      total        real
concat  1.406091   0.000476   1.406567 (  1.406721)
plus  1.295966   0.000044   1.296010 (  1.296153)
       user     system      total        real
concat  1.281295   0.000000   1.281295 (  1.281436)
plus  1.267036   0.000027   1.267063 (  1.267197)
       user     system      total        real
concat  1.291685   0.000003   1.291688 (  1.291826)
plus  1.266182   0.000000   1.266182 (  1.266320)
       user     system      total        real
concat  1.272261   0.000001   1.272262 (  1.272394)
plus  1.265784   0.000000   1.265784 (  1.265916)
       user     system      total        real
concat  1.272507   0.000001   1.272508 (  1.272646)
plus  1.294839   0.000000   1.294839 (  1.294974)

内存使用

require "benchmark/memory"

N = 10_000_00
Benchmark.memory do |x|
  x.report("array concat") { N.times { [].concat([4,5]) } }
  x.report("array +") { N.times { [] + [4, 5] } }

  x.compare!
end

Calculating -------------------------------------
        array concat    80.000M memsize (     0.000  retained)
                         2.000M objects (     0.000  retained)
                         0.000  strings (     0.000  retained)
             array +   120.000M memsize (     0.000  retained)
                         3.000M objects (     0.000  retained)
                         0.000  strings (     0.000  retained)

Comparison:
        array concat:   80000000 allocated
             array +:  120000000 allocated - 1.50x more

很好,但是你为什么要添加这个答案呢?这只不过是@the Tin Man的答案复制粘贴而已。https://dev59.com/eWAg5IYBdhLWcg3wS5WC#23764650 - Yurii Verbytskyi
1
@YuriiVerbytskyi 我回答的原因是因为Tinman的回答已经是6年前的了。所以,我想我可以提供最新版本Ruby的更新比较。如果有问题,我可以删除我的回答。 - Aniket Tiwari
1
这并不是问题,但是指出这些结果的原因而不仅仅是新基准会更有用。主要的问题在于这些操作创建的对象数量不同(在plus情况下创建了1.5倍以上的对象): ..times { [].concat([4,5]) } - 创建了2个对象([]和[4,5]),第一个返回带有更改。 - Yurii Verbytskyi
1
每次执行 times { [] + [4, 5] },都会返回第三个对象([][4,5][] + [4, 5] 新对象在每次迭代中创建)。 - Yurii Verbytskyi
1
你可以尝试添加一些关于内存分配方面的有用信息,例如基于这个宝石https://github.com/michaelherold/benchmark-memory。 - Yurii Verbytskyi
1
@YuriiVerbytskyi 我已经更新了答案,并提供了关于内存分配的相关信息。 - Aniket Tiwari

0
有趣的是,在对3种合并数组的变异方法进行基准测试时,我的基准测试表明,添加和重新分配方法实际上是最快的,差距恒定约为1%。(使用ruby 3.1.2)
基准测试:
A = [4, 5].freeze
N = 10_000_000

require 'benchmark'
Benchmark.bm do |b|
  b.report('plus  ') { N.times { c = [1, 2]; c = c + A } }
  b.report('concat') { N.times { [1, 2].concat(A) } }
  b.report('push  ') { N.times { [1, 2].push(*A) } }
end

结果:

       user     system      total        real
plus    1.180429   0.015026   1.195455 (  1.199552)
concat  1.228267   0.008172   1.236439 (  1.237004)
push    1.242709   0.007759   1.250468 (  1.251412)

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