我喜欢使用splat来构建数组和散列:
- 它们是数组和散列常量,所以你不需要进行一些计算就可以知道你得到了什么样的值,语法非常清楚
- 它们使在单个表达式中构建相当复杂的值变得容易,而不是使用命令式(是的,您可以将
tap
之类的内容转换为单个赋值语句,但可读性较差)。
然而,飞溅是昂贵的。
require 'benchmark'
$array = (0...100).to_a
n = 100_000
Benchmark.bm do |x|
x.report('add ') {n.times{$array + $array + $array}}
x.report('splat ') {n.times{[*$array, *$array, *$array]}}
end
在机器A(MRI 3.1.3)上,我有:
user system total real
add 0.031583 0.001421 0.033004 ( 0.033006)
splat 0.050174 0.001397 0.051571 ( 0.051584)
在机器B上(MRI 2.7.4):
user system total real
add 0.278377 0.000000 0.278377 ( 0.278316)
splat 0.780735 0.043730 0.824465 ( 0.824377)
基于splat的数组构造为什么这么慢?我希望基于splat的构造不会比普通加法慢(毕竟AST甚至可以将一个转换为另一个),而且我实际上希望它更高效(因为该语言可以看到所有内容,所以它可以避免二进制加法创建的中间数组,它还可以预测最终数组的大小并预先预留空间,等等)。
那么,为什么抛出一个方法调用(因此,先验地,解释器不太可能优化)的替代方法比所有内容都诚实地暴露给解释器的方法更快呢?
EDIT:更多备选方案
require 'benchmark'
$array = (0...100).to_a
def add
$array + $array + $array
end
def append
res = $array.dup
res.append(*$array)
res.append(*$array)
res
end
def concat2
res = []
res.concat($array)
res.concat($array)
res.concat($array)
res
end
def concat3
[].concat($array, $array, $array)
end
def concat_splat
[].concat(*[$array, $array, $array])
end
def flatten
[$array, $array, $array].flatten
end
def flatten_1
[$array, $array, $array].flatten(1)
end
def splat
[*$array, *$array, *$array]
end
n = 100_000
Benchmark.bm do |x|
x.report('add ') {n.times{add}}
x.report('append ') {n.times{append}}
x.report('concat2 ') {n.times{concat2}}
x.report('concat3 ') {n.times{concat3}}
x.report('concat_splat') {n.times{concat_splat}}
x.report('flatten ') {n.times{flatten}}
x.report('flatten(1) ') {n.times{flatten_1}}
x.report('splat ') {n.times{splat}}
end
这是机器A,MRI 3.1.3。
user system total real
add 0.032841 0.001502 0.034343 ( 0.034347)
append 0.059024 0.009869 0.068893 ( 0.068944)
concat2 0.047542 0.000144 0.047686 ( 0.047690)
concat3 0.062913 0.010196 0.073109 ( 0.073111)
concat_splat 0.056044 0.000748 0.056792 ( 0.056796)
flatten 0.978091 0.005750 0.983841 ( 0.983952)
flatten(1) 0.165467 0.000998 0.166465 ( 0.166472)
splat 0.049761 0.000131 0.049892 ( 0.049896)
1条答案
按热度按时间fzsnzjdm1#
加法和splat版本发出不同的字节码(为了简洁,省略了一些输出):
对比
上面的两个片段看起来非常相似,区别是2
ops_plus
指令与splatarray
+ 2concatarray
指令。但在实现方面,差异变得更大。第一个可以归结为2
rb_ary_plus
,简而言之:后者内部似乎更为复杂:
splatarray
归结为rb_ary_dup
(所以我们先复制ary),concatarray
也在幕后复制一个目标数组,然后归结为rb_ary_splice
;后者有点麻烦,但我相信我们可以转到这个分支,在那里我们有效地将数组容量加倍(包括复制第一个数组),然后复制第二个数组。我不能100%确定我是否正确地跟踪了这个执行流,但如果我这样做了,我们将得到:这些额外的重复可以解释这种差异(更不用说后者的整体复杂性,这意味着检查了更多的条件句等)。