【发布时间】:2019-05-08 11:43:41
【问题描述】:
有谁知道 ruby 数组中 shift 和 unshift 的效率如何?
从数组的开头删除并且必须移动内存中的每个元素可能会变得非常低效。我认为 ruby 以其他方式做到这一点。
以下任何信息都会有所帮助:
- 算法运行时
- 实施
- 一般效率
- 将 shift/unshift 用于队列是否可以接受(在 C++ 之类的东西中,这不会)
谢谢!
【问题讨论】:
有谁知道 ruby 数组中 shift 和 unshift 的效率如何?
从数组的开头删除并且必须移动内存中的每个元素可能会变得非常低效。我认为 ruby 以其他方式做到这一点。
以下任何信息都会有所帮助:
- 算法运行时
- 实施
- 一般效率
- 将 shift/unshift 用于队列是否可以接受(在 C++ 之类的东西中,这不会)
谢谢!
【问题讨论】:
在旧版本的 Ruby(~2012 之前)中,unshift 是一个 O(n) 操作。但是,this commit 和 released in Ruby 2.0.0 中添加了优化,这使得 unshift 摊销 O(1),这意味着它保证平均为 O(1),但单个操作可能是 O(n)。这与shift 的运行时间相同。
This CS Stack Exchange post 很好地解释了它是如何工作的,以及你如何最终得到 O(1) 的摊销运行时间(这是关于 C++ 的 vector::push_back,但它的工作方式相同)。
【讨论】:
您可以查看here 并查看unshift 方法的C 源代码(只需单击描述块)。很清楚:如果我们还没有足够的内存容量,则增加内存容量,向前移动数组的当前内容,将传递的参数复制到内存块开头的空闲空间。所以O(n) 是unshift。
【讨论】:
我发现回答这个问题的最简单和最明确的方法是对其进行基准测试。
require 'benchmark'
Benchmark.bm do |x|
iterations = 10000000
x.report("push") {
a = []
iterations.times do a.push(10) end
}
x.report("unshift") {
a = []
iterations.times do a.unshift(10) end
}
a = []
iterations.times do a.push(10) end
x.report("shift") {
iterations.times do a.shift() end
}
a = []
iterations.times do a.push(10) end
x.report("pop") {
iterations.times do a.pop() end
}
end
在我的系统上运行 ruby 版本 2.0.0 这将返回结果:
user system total real
push 0.880000 0.030000 0.910000 ( 0.917213)
unshift 0.920000 0.090000 1.010000 ( 1.026208)
shift 0.780000 0.030000 0.810000 ( 0.810293)
pop 0.710000 0.000000 0.710000 ( 0.724865)
push、pop、shift 和 unshift 似乎都花费了大致相同的时间。
使用 iterations 的不同值再次运行此代码会得到与我更改 iterations 的数量成比例的结果。这意味着无论iterations 的值如何,每个操作的平均时间总是相同的,这意味着每个操作的运行时间与数组的长度无关,因此具有O(1) 的运行时间。
我会说这可以作为队列使用。
【讨论】:
iterations 时,它们似乎都是线性缩放的”,这意味着当我尝试将代码中 iterations 的值加倍时,所有时间结果也翻了一番。我想我应该更清楚,并将我的结果包含在不同的迭代值中。我想我没有这样做是因为我不想让评论变得混乱(我不记得我两年前的推理了)。但我确实相信这是测试每个操作的运行时间的有效方法。 (在下一条评论中继续)
push 的运行时间相对于数组的长度是恒定的,因为数组的平均长度随迭代线性缩放。
根据this article,它似乎并没有真正改变,只是增加一个指针并返回它。所以就效率而言,它的效率非常高(O(1))。然而,文章提到了一个潜在的内存泄漏,这在最近的版本中可能存在也可能不存在。
【讨论】:
shift 方法的实现变得有点复杂,在某些情况下它确实会移动内存。它与使用的指针类型有关,但我不太确定这个ARY_SHARED_P(ary) 何时为真,何时不是。