aki*_*kim 3 ruby arrays performance
我喜欢使用 splat 构建数组和哈希:
\ntap,但它的可读性较差)。然而,泼溅的成本很高。
\nrequire \'benchmark\'\n\n$array = (0...100).to_a\n\nn = 100_000\nBenchmark.bm do |x|\n x.report(\'add \') {n.times{$array + $array + $array}}\n x.report(\'splat \') {n.times{[*$array, *$array, *$array]}}\nend\nRun Code Online (Sandbox Code Playgroud)\n在机器 A (MRI 3.1.3) 上我有:
\n user system total real\nadd 0.031583 0.001421 0.033004 ( 0.033006)\nsplat 0.050174 0.001397 0.051571 ( 0.051584)\nRun Code Online (Sandbox Code Playgroud)\n在机器 B (MRI 2.7.4) 上:
\n user system total real\nadd 0.278377 0.000000 0.278377 ( 0.278316)\nsplat 0.780735 0.043730 0.824465 ( 0.824377)\nRun Code Online (Sandbox Code Playgroud)\n为什么基于 splat 的数组构建如此慢?我预计基于 splat 的构造不会比普通加法慢(毕竟 AST 甚至可以将一个转换为另一个),而且我实际上期望它更高效(因为该语言可以看到所有内容,因此它可以避免中间过程)通过二进制加法创建的数组,它还可以预测最终数组的大小并预先预留空间等)。
\n那么,为什么 go 抛出方法调用(因此,先验地,解释器不太可优化)的替代方案比将所有内容诚实地暴露给解释器的方法更快呢?
\n user system total real\nadd 0.031583 0.001421 0.033004 ( 0.033006)\nsplat 0.050174 0.001397 0.051571 ( 0.051584)\nRun Code Online (Sandbox Code Playgroud)\n这是机器 A,MRI 3.1.3。
\nWarming up --------------------------------------\n add 300.630k i/100ms\n append 140.913k i/100ms\n concat2 154.698k i/100ms\n concat3 120.459k i/100ms\n concat_splat 142.808k i/100ms\n flatten 10.329k i/100ms\n flatten(1) 52.207k i/100ms\n splat 195.946k i/100ms\nCalculating -------------------------------------\n add 3.040M (\xc2\xb1 0.7%) i/s - 15.332M in 5.043760s\n append 1.400M (\xc2\xb1 1.9%) i/s - 7.046M in 5.034067s\n concat2 1.532M (\xc2\xb1 2.1%) i/s - 7.735M in 5.049821s\n concat3 1.134M (\xc2\xb1 2.6%) i/s - 5.782M in 5.101784s\n concat_splat 1.409M (\xc2\xb1 1.9%) i/s - 7.140M in 5.068373s\n flatten 102.948k (\xc2\xb1 0.6%) i/s - 516.450k in 5.016786s\n flatten(1) 517.582k (\xc2\xb1 5.2%) i/s - 2.610M in 5.058161s\n splat 1.939M (\xc2\xb1 1.4%) i/s - 9.797M in 5.052514s\n\nComparison:\n add: 3039958.8 i/s\n splat: 1939484.0 i/s - 1.57x (\xc2\xb1 0.00) slower\n concat2: 1532384.2 i/s - 1.98x (\xc2\xb1 0.00) slower\n concat_splat: 1409339.8 i/s - 2.16x (\xc2\xb1 0.00) slower\n append: 1400120.7 i/s - 2.17x (\xc2\xb1 0.00) slower\n concat3: 1134080.2 i/s - 2.68x (\xc2\xb1 0.00) slower\n flatten(1): 517582.1 i/s - 5.87x (\xc2\xb1 0.00) slower\n flatten: 102948.3 i/s - 29.53x (\xc2\xb1 0.00) slower\nRun Code Online (Sandbox Code Playgroud)\n
Addition 和 splat 版本发出不同的字节码(为简洁起见,省略了一些输出):
puts RubyVM::InstructionSequence.compile(<<~ADDITION).disasm
src = (0...100).to_a
res = src + src + src
ADDITION
0000 putobject 0...100 ( 1)[Li]
0002 opt_send_without_block <calldata!mid:to_a, argc:0, ARGS_SIMPLE>
0004 setlocal_WC_0 src@0
0006 getlocal_WC_0 src@0 ( 2)[Li]
0008 getlocal_WC_0 src@0
0010 opt_plus <calldata!mid:+, argc:1, ARGS_SIMPLE>
0012 getlocal_WC_0 src@0
0014 opt_plus <calldata!mid:+, argc:1, ARGS_SIMPLE>
0016 dup
0017 setlocal_WC_0 res@1
0019 leave
Run Code Online (Sandbox Code Playgroud)
与
puts RubyVM::InstructionSequence.compile(<<~SPLATS).disasm
src = (0...100).to_a
res = [*src, *src, *src]
SPLATS
0000 putobject 0...100 ( 1)[Li]
0002 opt_send_without_block <calldata!mid:to_a, argc:0, ARGS_SIMPLE>
0004 setlocal_WC_0 src@0
0006 getlocal_WC_0 src@0 ( 2)[Li]
0008 splatarray true
0010 getlocal_WC_0 src@0
0012 concatarray
0013 getlocal_WC_0 src@0
0015 concatarray
0016 dup
0017 setlocal_WC_0 res@1
0019 leave
Run Code Online (Sandbox Code Playgroud)
上面的两个片段看起来非常相似,区别在于 2ops_plus条指令与splatarray+ 2concatarray条指令。但在实施方面,差异变得更大。
rb_ary_plus简而言之,第一个归结为 2 :
后者在内部似乎更复杂:splatarray归结为rb_ary_dup(因此我们首先复制 ary),concatarray在幕后也复制目标数组,然后归结为rb_ary_splice; 后者有点毛茸茸的,但我相信我们进入这个分支,我们有效地将数组容量加倍(包括复制第一个数组),然后复制第二个数组。我不能 100% 确定我是否正确地跟踪了这个执行流程,但如果我这样做了,它会给我们带来:
这些额外的重复可以解释这种差异(更不用说后者的整体复杂性意味着检查更多的条件等)。