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版本.
解决方法
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,4,5] b = a.dup ObjectSpace.memsize_of(b) #=> 40
这里b分享一个缓冲区,所以报告为使用没有超出基本对象大小的内存.在b被写入的地方,ruby将不得不复制数据(写在副本上):在第一个基准BOTH =和<<实际上正在分配一个足够大的新的缓冲区并复制所有的数据. 这里是我得到handwavy:这将完全解释的东西,如果<和=执行相同,但这不是发生了什么.我读的东西是简单的.所有它必须做的,无论什么是分配缓冲区,并从两个位置memcpy一些数据 - 这是快速. <<另一方面是使数组变异,因此它在写入时支付拷贝的开销:它正在做额外的工作与=比较.例如,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
如果你这样做两次,那么附加到数组就返回顶部了.