为什么在连接列表时Enum.concat比++慢得多?

Vit*_*upt 10 benchmarking elixir

我尝试使用Benchfella进行一些快速基准测试:

defmodule ConcatListBench do
  use Benchfella

  @a1 Enum.to_list(1..10_000)
  @a2 Enum.to_list(10_000..20_0000)

  bench "++" do
    @a1 ++ @a2
  end

  bench "Enum.concat" do
    Enum.concat(@a1, @a2)
  end
end
Run Code Online (Sandbox Code Playgroud)

在运行时:

$ elixir -v
Erlang/OTP 19 [erts-8.0.2] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]
Elixir 1.4.0-dev (762e7de)

$ mix bench
Settings:
  duration:      1.0 s

## ConcatListBench
[10:01:09] 1/2: ++
[10:01:20] 2/2: Enum.concat

Finished in 14.03 seconds

## ConcatListBench
benchmark na iterations   average time
++           1000000000   0.01 µs/op
Enum.concat       50000   45.03 µs/op
Run Code Online (Sandbox Code Playgroud)

问题是Enum.concat如果内部使用 ++运算符列表,怎么会更慢(超过4k次)?

我理解守护条款Enum.concat和模式匹配花费了一些时间,但基准显示出很大的差异,不是吗?

更新:这是由于常量折叠,++在编译时使用优化的串联并需要立即运行.所以基准测试不太现实.

Dog*_*ert 17

简短回答:不断折叠.

更长的答案:当Elixir编译为beam文件时,Elixir中的模块属性将替换为其文字值.例如,以下代码:

defmodule ConcatListBench do
  @a1 Enum.to_list(1..10)
  @a2 Enum.to_list(10..20)

  def plusplus, do: @a1 ++ @a2

  def concat, do: Enum.concat(@a1, @a2)
end
Run Code Online (Sandbox Code Playgroud)

编译为:

-module('Elixir.ConcatListBench').
... 
concat() ->
    'Elixir.Enum':concat([1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
             [10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]).

plusplus() ->
    [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] ++
      [10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20].
Run Code Online (Sandbox Code Playgroud)

Erlang编译器的sys_core_fold模块执行常量折叠优化,在编译时尽可能地评估++操作.因为在这种情况下,两个列表都是文字,它可以完全消除函数调用并将其替换为结果列表.因此,在您的基准测试中,该++函数只返回VM中已存在的列表.它和做的一样快1 + 2(也是常数折叠3):

...
bench "1 + 2" do
  1 + 2
end
...
Run Code Online (Sandbox Code Playgroud)
## ConcatListBench
benchmark na iterations   average time
1 + 2        1000000000   0.01 µs/op
++           1000000000   0.01 µs/op
Enum.concat       50000   37.89 µs/op
Run Code Online (Sandbox Code Playgroud)

更现实的基准是进行间接调用++,Erlang编译器不会折叠:

def plus_plus(a, b), do: a ++ b

bench "++" do
  plus_plus(@a1, @a2)
end
Run Code Online (Sandbox Code Playgroud)

这些是3次运行的输出:

## ConcatListBench
benchmark na iterations   average time
Enum.concat       50000   37.44 µs/op
++                50000   41.65 µs/op

## ConcatListBench
benchmark na iterations   average time
++                50000   36.07 µs/op
Enum.concat       50000   38.58 µs/op

## ConcatListBench
benchmark na iterations   average time
Enum.concat       50000   39.34 µs/op
++                50000   40.74 µs/op
Run Code Online (Sandbox Code Playgroud)

实际上,如果你的列表在编译时不是常量,那么两种方式都同样快.我希望Enum.concat它会慢一点(特别是对于小型列表),因为它做的工作要多一些++.