【问题标题】:Running time Array#unshift vs Array#shift运行时间 Array#unshift 与 Array#shift
【发布时间】:2014-08-24 23:30:39
【问题描述】:

我预计Array#shiftArray#unshift 的运行时间都是Θ(n)。原因是机器需要循环遍历每个数组成员并将其分配给左侧或右侧的键。

Array#unshift的情况下,假设只有一个值作为参数传入并且有很多数组成员,我假设array[0]的值分配对运行没有显着影响时间。换句话说,当数组成员的数量多而传递给Array#unshift的变量数量少时,我希望Array#shiftArray#unshift具有相同的运行时间。

在 Ruby 2.1.2 上运行基准测试时,这些假设不成立。为什么?

代码:

require 'benchmark'

GC.disable    

number_of_elements = 25_600_000

a1 =[]
a2 = []
a3 = []
a4 = []
q1 = Queue.new
q2 = Queue.new

puts number_of_elements

number_of_elements.times do
  q1.enq(true)
  q2.enq(true)
  a1 << true
  a2 << true
  a3 << true
  a4 << true
end

number_of_operations = 1

Benchmark.bm do |bm|
  puts "Queue#enq('test')"
  bm.report do    
    number_of_operations.times { q1.enq('test') }
  end

  puts "Queue#deq"
  bm.report do    
    number_of_operations.times { q2.deq }
  end

  puts "Array#shift"
  bm.report do
    number_of_operations.times { a1.shift }
  end 

  puts "Array#unshift"
  bm.report do
    number_of_operations.times { a2.unshift('test') }    
  end 

  puts "Array#pop"
  bm.report do
    number_of_operations.times { a3.pop }
  end

  puts "Array#<<"
  bm.report do
    number_of_operations.times { a4 << 'test' }
  end      
end

结果:

25600000
       user     system      total        real
Queue#enq('test')
   0.000000   0.000000   0.000000 (  0.000006)
Queue#deq
   0.010000   0.020000   0.030000 (  0.029928)
Array#shift
   0.010000   0.020000   0.030000 (  0.032203)
Array#unshift
   0.080000   0.060000   0.140000 (  0.143272)
Array#pop
   0.000000   0.000000   0.000000 (  0.000004)
Array#<<
   0.000000   0.000000   0.000000 (  0.000007)

【问题讨论】:

  • 如果增加操作次数会发生什么?
  • #shift@12.8m:0.016307,#shift@25.6m:0.059878,#shift@64m:0.098583,#shift@128m:0.344900。 #unshift@12.8m:0.059736,#unshift@25.6m:0.126382,#unshift@64m:0.285351,#unshift@128m:0.993967。通过仅比较这些比率,似乎#shift 运行在 Θ(3.4n) 和 #unshift 略微指数。
  • 您是否研究过 Ruby 源代码以了解数组是如何实现的以及 shift/unshift 是如何工作的?幕后可能发生的各种事情都会影响您的结果。将元素添加到数组的开头并不一定会为数组分配一个新条目,它可能会分配很多,或者它可能只是移动一个 array-starts-here 指针;同样,shifting 一个元素可能只是更新一个指向新 index-0 地址的指针。
  • 我看了看,但它是用 C 语言编写的,零 C 经验我很难理解。
  • 你的问题根本没用,不提你用的是哪个 ruby​​ 版本

标签: ruby arrays performance


【解决方案1】:

在 MRI Ruby 2.1.2 中,unshift 会重新分配数组并完全复制它:

              static VALUE
rb_ary_unshift_m(int argc, VALUE *argv, VALUE ary)
{
    long len = RARRAY_LEN(ary);

    [...]

    ary_ensure_room_for_unshift(ary, argc);
    ary_memcpy(ary, 0, argc, argv);
    ARY_SET_LEN(ary, len + argc);
    return ary;
}

shift 显然并不总是这样做:

              static VALUE
rb_ary_shift_m(int argc, VALUE *argv, VALUE ary)
{
    VALUE result;
    long n;

    [...]

    rb_ary_modify_check(ary);
    result = ary_take_first_or_last(argc, argv, ary, ARY_TAKE_FIRST);
    n = RARRAY_LEN(result);
    if (ARY_SHARED_P(ary)) {
        if (ARY_SHARED_OCCUPIED(ARY_SHARED(ary))) {
            ary_mem_clear(ary, 0, n);
        }
        ARY_INCREASE_PTR(ary, n);
    }
    else {
        RARRAY_PTR_USE(ary, ptr, {
            MEMMOVE(ptr, ptr + n, VALUE, RARRAY_LEN(ary)-n);
        }); /* WB: no new reference */
    }
    ARY_INCREASE_LEN(ary, -n);

    return result;
}

【讨论】:

  • 在某些情况下,数组前面有额外的空间分配用于取消移位:github.com/ruby/ruby/commit/…
  • 以上是2.0的,所以这个答案有点误导。似乎对于小于 64 的数组大小,这种优化不会发生,大概是因为当 n 时 O(n) 并不是那么糟糕
猜你喜欢
  • 1970-01-01
  • 2018-03-02
  • 2011-07-12
  • 2016-09-12
  • 1970-01-01
  • 2020-04-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多