什么在Ruby中更快,`arr + = [x]`或`arr<< x`

时间:2015-12-10 16:53:24

标签: arrays ruby performance benchmarking microbenchmark

直观地说,后者应该比前者更快。但是,当我看到基准测试结果时,我感到非常惊讶:

  require 'benchmark/ips'

  b = (0..20).to_a;
  y = 21;
  Benchmark.ips do |x|
    x.report('<<')   { a = b.dup; a << y }
    x.report('+=')   { a = b.dup; a += [y] }
    x.report('push') { a = b.dup; a.push(y) }
    x.report('[]=')  { a = b.dup; a[a.size]=y }
    x.compare!
  end

结果是:

Calculating -------------------------------------
                  <<    24.978k i/100ms
                  +=    30.389k i/100ms
                push    24.858k i/100ms
                 []=    22.306k i/100ms
-------------------------------------------------
                  <<    493.125k (± 3.2%) i/s -      2.473M
                  +=    599.830k (± 2.3%) i/s -      3.009M
                push    476.374k (± 3.3%) i/s -      2.386M
                 []=    470.263k (± 3.8%) i/s -      2.364M

Comparison:
                  +=:   599830.3 i/s
                  <<:   493125.2 i/s - 1.22x slower
                push:   476374.0 i/s - 1.26x slower
                 []=:   470262.8 i/s - 1.28x slower

但是,当我的一位同事独立创建自己的基准时,结果恰恰相反:

 Benchmark.ips do |x|
   x.report('push') {@a = (0..20).to_a; @a.push(21)}
   x.report('<<')   {@b = (0..20).to_a; @b << 21}
   x.report('+=')   {@c = (0..20).to_a; @c += [21]}
   x.compare!
 end

结果:

Calculating -------------------------------------
                push    17.623k i/100ms
                  <<    18.926k i/100ms
                  +=    16.079k i/100ms
-------------------------------------------------
                push    281.476k (± 4.2%) i/s -      1.410M
                  <<    288.341k (± 3.6%) i/s -      1.457M
                  +=    219.774k (± 8.3%) i/s -      1.093M

Comparison:
                  <<:   288341.4 i/s
                push:   281476.3 i/s - 1.02x slower
                  +=:   219774.1 i/s - 1.31x slower

我们也交叉运行我们的基准测试,在我们的机器上,他的基准测试显示+=明显慢于<<,而我的基准显示相反。

为什么?

UPD:我的Ruby版本是 Ruby 2.2.3p173(2015-08-18修订版51636)[x86_64-darwin14] ;我的同事是 2.2.2 (不知道详情,明天会更新帖子。)

UPD2: ruby​​ 2.2.2p95(2015-04-13修订版50295)[x86_64-darwin12.0] 我的队友的Ruby版本。

2 个答案:

答案 0 :(得分:5)

在我看来,为了简化各种运算符的比较,我们应该删除不必要的代码并保持测试的简单。

require 'benchmark/ips'

y = 10
Benchmark.ips do |x|
    x.report('<<')   { a = [0,1,2,3,4,5,6,7,8,9]; a << y }
    x.report('+=')   { a = [0,1,2,3,4,5,6,7,8,9]; a += [y] }
    x.report('push') { a = [0,1,2,3,4,5,6,7,8,9]; a.push(y) }
    x.report('[]=')  { a = [0,1,2,3,4,5,6,7,8,9]; a[a.size]=y }
    x.compare!
end

上述代码的结果与问题中分享的第二个代码段一致。

Calculating -------------------------------------
                  <<   101.735k i/100ms
                  +=   104.804k i/100ms
                push    92.863k i/100ms
                 []=    99.604k i/100ms
-------------------------------------------------
                  <<      2.134M (± 3.3%) i/s -     10.682M
                  +=      1.786M (±13.2%) i/s -      8.804M
                push      1.930M (±16.1%) i/s -      9.472M
                 []=      1.948M (± 7.9%) i/s -      9.761M

Comparison:
                  <<:  2134005.4 i/s
                 []=:  1948256.8 i/s - 1.10x slower
                push:  1930165.3 i/s - 1.11x slower
                  +=:  1785808.5 i/s - 1.19x slower

[Finished in 28.3s]

为什么<<+=快?

Array#<<是将元素附加到数组的四种方法中最快的,因为它只是这样做 - 将一个元素附加到数组中。相反,Array#+附加一个元素但返回数组的新副本 - 创建新的数组副本使其最慢。 (可以在文档中使用toogle code选项来了解某些方法所做的其他工作)

使用dup

进行基准测试

如果我们使用以下代码进行基准测试,

require 'benchmark/ips'

y = 10
Benchmark.ips do |x|
    x.report('<<')   { a = [0,1,2,3,4,5,6,7,8,9].dup; a << y }
    x.report('+=')   { a = [0,1,2,3,4,5,6,7,8,9].dup; a += [y] }
    x.report('push') { a = [0,1,2,3,4,5,6,7,8,9].dup; a.push(y) }
    x.report('[]=')  { a = [0,1,2,3,4,5,6,7,8,9].dup; a[a.size]=y }
    x.compare!
end

我们看到以下结果:

Calculating -------------------------------------
                  <<    65.225k i/100ms
                  +=    76.106k i/100ms
                push    64.864k i/100ms
                 []=    63.582k i/100ms
-------------------------------------------------
                  <<      1.221M (±14.3%) i/s -      6.001M
                  +=      1.291M (±13.1%) i/s -      6.393M
                push      1.164M (±14.1%) i/s -      5.773M
                 []=      1.168M (±14.5%) i/s -      5.722M

Comparison:
                  +=:  1290970.6 i/s
                  <<:  1221029.0 i/s - 1.06x slower
                 []=:  1168219.3 i/s - 1.11x slower
                push:  1163965.9 i/s - 1.11x slower

[Finished in 28.3s]

如果我们仔细研究两个结果,我们只看到一个区别。 +=条目已成为第一个,而其余方法的顺序与原始结果保持一致。

为什么在使用dup时结果会翻转?

这是我疯狂的猜测,我猜测Ruby解释器优化了代码,并没有创建一个新的数组作为+=的一部分,因为它知道它正在处理新创建的数组副本{{1 }}

答案 1 :(得分:2)

我认为这取决于MRI如何分配阵列(所有这些答案都非常具有MRI特异性)。 Ruby尝试使用数组非常有效:例如,小数组(&lt; = 3个元素)直接打包到RARRAY结构中。

另一件事是,如果你拿一个数组并开始一次附加一个值,ruby不会一次增加一个元素的缓冲区,它会以块的形式增加:这样做更有效率,代价是少量记忆。

一个看到这一切的工具是使用memsize_of:

ObjectSpace.memspace_of([]) #=> 40 (on 64 bit platforms
ObjectSpace.memspace_of([1,2]) #=> 40 (on 64 bit platforms
ObjectSpace.memsize_of([1,2,3,4]) #=> 72
ObjectSpace.memsize_of([]<<1<<2<<3<<4) #=> 200

在前两种情况下,数组在RARRAY结构中打包,因此内存大小只是任何对象的基本大小(40字节)。在第三种情况下,ruby必须为4个值(每个8个字节)分配一个数组,因此大小为40 + 32 = 72.在最后一种情况下,ruby将存储增加到20个元素

这与第二种情况有关。基准测试中的块有一个新创建的阵列,它仍然有一些备用容量:

 ObjectSpace.memsize_of((0..20).to_a) #=> 336, enough for nearly 40 elements.

<<可以将其对象写入适当的插槽,而+=必须分配一个新数组(对象及其缓冲区)并复制所有数据。

如果我这样做

a = [1,2,3,4,5]
b  = a.dup
ObjectSpace.memsize_of(b) #=> 40

此处ba共享其缓冲区,因此报告为不使用超出基本对象大小的内存。在写入b时,ruby必须复制数据(写入时复制):在第一个基准 BOTH +=<<中实际上,分配一个足够大的新缓冲区并复制所有数据。

这是我手绘的地方:如果<<+=表现相同,这将完全解释事情,但这不是正在发生的事情。我读到的事情是+更简单。所有这一切都需要做,无论分配缓冲区是什么,并从两个位置记忆一些数据 - 这很快。

另一方面,

<<正在改变数组,因此它支付了写入时复制的开销:与+=相比,它正在做额外的工作。例如,ruby需要跟踪共享缓冲区的人员,以便在没有人共享缓冲区时可以对原始数组进行垃圾收集。

一种基准,可以说服我这种解释是正确的如下:

require 'benchmark/ips'
b = (0..20).to_a.dup
y = 21
Benchmark.ips do |x|
  x.report('<<')   { a = b.dup; a << y }
  x.report('+=')   { a = b.dup; a += [y] }

  x.report('<<2')   { a = b.dup; a << y; a<< y}
  x.report('+=2')   { a = b.dup; a += [y]; a += [y] }
end

这与原版基本相同,但现在附加2个元素。对于<<,写入开销的复制只会在第一次产生。我得到的结果是

              <<      1.325M (± 7.6%) i/s -      6.639M
              +=      1.742M (± 9.5%) i/s -      8.677M
             <<2      1.230M (±10.3%) i/s -      6.079M
             +=2      1.140M (±10.8%) i/s -      5.656M

如果你做两次,那么附加到数组会重新登上。