为什么 Enum.concat 在连接列表时比 ++ 慢得多?
Why Enum.concat is much more slower than ++ while concatenating lists?
我尝试使用 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
运行宁它时:
$ 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
问题是,如果 it uses ++
运算符在列表内部,Enum.concat
怎么会变慢(超过 4k 倍)?
我知道 Enum.concat
中的保护子句和模式匹配需要一些时间,但基准测试显示有很大差异,不是吗?
更新: 发生这种情况是由于 Constant Folding,使用 ++
的串联在编译时进行了优化,并且需要即时时间 运行。所以基准不太现实。
简答:Constant Folding.
更长的答案:当 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
编译为:
-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].
Erlang 编译器的 sys_core_fold
module, which does constant folding optimization, evaluates ++
operations as much as possible at compile time。由于在这种情况下,两个列表都是文字,因此可以完全消除函数调用并将其替换为结果列表。因此,在您的基准测试中,++
函数只是返回一个已存在于 VM 中的列表。它和 1 + 2
一样快(它也常量折叠到 3
):
...
bench "1 + 2" do
1 + 2
end
...
## 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
一个更现实的基准是对 Erlang 编译器不会折叠的 ++
进行间接调用:
def plus_plus(a, b), do: a ++ b
bench "++" do
plus_plus(@a1, @a2)
end
这些是 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
实际上,如果您的列表在编译时不是常量,那么这两种方法都一样快。我希望 Enum.concat
会稍微慢一点(尤其是对于小列表),因为它比 ++
.
做更多的工作
我尝试使用 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
运行宁它时:
$ 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
问题是,如果 it uses ++
运算符在列表内部,Enum.concat
怎么会变慢(超过 4k 倍)?
我知道 Enum.concat
中的保护子句和模式匹配需要一些时间,但基准测试显示有很大差异,不是吗?
更新: 发生这种情况是由于 Constant Folding,使用 ++
的串联在编译时进行了优化,并且需要即时时间 运行。所以基准不太现实。
简答:Constant Folding.
更长的答案:当 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
编译为:
-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].
Erlang 编译器的 sys_core_fold
module, which does constant folding optimization, evaluates ++
operations as much as possible at compile time。由于在这种情况下,两个列表都是文字,因此可以完全消除函数调用并将其替换为结果列表。因此,在您的基准测试中,++
函数只是返回一个已存在于 VM 中的列表。它和 1 + 2
一样快(它也常量折叠到 3
):
...
bench "1 + 2" do
1 + 2
end
...
## 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
一个更现实的基准是对 Erlang 编译器不会折叠的 ++
进行间接调用:
def plus_plus(a, b), do: a ++ b
bench "++" do
plus_plus(@a1, @a2)
end
这些是 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
实际上,如果您的列表在编译时不是常量,那么这两种方法都一样快。我希望 Enum.concat
会稍微慢一点(尤其是对于小列表),因为它比 ++
.