【问题标题】:Elixir Enum.map vs For comprehensionElixir Enum.map vs For 理解
【发布时间】:2022-01-20 22:41:26
【问题描述】:

我有一张地图,我正在修改上面的每个元素,我很困惑哪种方法更好(更快)使用Enum.map 然后Enum.into(%{}) 或用于理解,如

for {key, value} <- my_map, into: %{} do
  {key, new_value}
end

【问题讨论】:

  • 如果你问得更快,究竟是什么阻碍了你进行测试和测量?

标签: functional-programming elixir


【解决方案1】:

原答案

您可以使用Benchee 进行此类比较。

一个简单的 Benchee 测试将表明,Enum 在这种情况下更快。

iex(1)> m = %{a: 1, b: 2, c: 3, d: 4}
%{a: 1, b: 2, c: 3, d: 4}
iex(2)> with_enum = fn -> Enum.map(m, fn {k, v} -> {k, v * v} end) end
#Function<20.127694169/0 in :erl_eval.expr/5>
iex(3)> with_for = fn -> for {k, v} <- m, into: %{}, do: {k, v * v} end
#Function<20.127694169/0 in :erl_eval.expr/5>
iex(4)> Benchee.run(%{
...(4)>   "with_enum" => fn -> with_enum.() end,
...(4)>   "with_for" => fn -> with_for.() end
...(4)> })
Operating System: Linux
CPU Information: Intel(R) Core(TM) i7-6500U CPU @ 2.50GHz
Number of Available Cores: 4
Available memory: 7.71 GB
Elixir 1.7.4
Erlang 21.0

Benchmark suite executing with the following configuration:
warmup: 2 s
time: 5 s
memory time: 0 ns
parallel: 1
inputs: none specified
Estimated total run time: 14 s


Benchmarking with_enum...
Benchmarking with_for...

Name                ips        average  deviation         median         99th %
with_enum       28.27 K       35.37 μs    ±16.16%       34.37 μs       55.21 μs
with_for        19.55 K       51.14 μs     ±9.16%       50.08 μs       59.94 μs

Comparison: 
with_enum       28.27 K
with_for        19.55 K - 1.45x slower

一般来说,for 不是 Elixir 中这些情况的最佳选择,它最适合列表推导,它可以非常快速地完成并且具有易于阅读的语法。

Enum 的函数经过优化,可以处理这些更具迭代性的场景,就像您在其他编程语言中使用 for 构造所做的那样。


编辑

尽管我最初回答的主要目的是指向一个有助于运行此类比较的框架,以便 OP 可以自己尝试,正如另一位用户所指出的那样,使用 Enum.map 的示例函数不是' t 产生与for 相同的结果。正如他自己所指出的,将Enum.into 添加到Enum.map 调用会导致函数运行时间有时更长。所以这里有一个更新,添加了一些更多的选项,这些选项也可以被认为产生相同的结果,并带有一个基准。

iex> m = %{a: 1, b: 2, c: 3, d: 4}
%{a: 1, b: 2, c: 3, d: 4}
iex> with_enum_map_into = fn -> m |> Enum.map(fn {k, v} -> {k, v * v} end) |> Enum.into(%{}) end
#Function<...>
iex> with_enum_map_map_new = fn -> m |> Enum.map(fn {k, v} -> {k, v * v} end) |> Map.new() end
#Function<...>
iex> with_map_new = fn -> Map.new(m, fn {k, v} -> {k, v * v} end) end
#Function<...>
iex> with_reduce_map_put = fn -> Enum.reduce(m, %{}, fn {k, v}, acc -> Map.put(acc, k, v * v) end) end
#Function<...>
iex> with_reduce_map_merge = fn -> Enum.reduce(m, %{}, fn {k, v}, acc -> Map.merge(acc, %{k => v * v}) end) end
#Function<...>
iex> with_for = fn -> for {k, v} <- m, into: %{}, do: {k, v * v} end
#Function<20.127694169/0 in :erl_eval.expr/5>
iex> Benchee.run(%{                                               
...>   "with_for" => fn -> with_for.() end,                       
...>   "with_enum_map_into" => fn -> with_enum_map_into.() end,   
...>   "with_enum_map_map_new" => fn -> with_enum_map_map_new.() end,
...>   "with_map_new" => fn -> with_map_new.() end,                  
...>   "with_reduce_map_put" => fn -> with_reduce_map_put.() end,    
...>   "with_reduce_map_merge" => fn -> with_reduce_map_merge.() end 
...> })
Benchmarking with_enum_map_into...
Benchmarking with_enum_map_map_new...
Benchmarking with_for...
Benchmarking with_map_new...
Benchmarking with_reduce_map_merge...
Benchmarking with_reduce_map_put...

Name                            ips        average  deviation         median         99th %
with_enum_map_map_new       96.55 K       10.36 μs   ±158.95%        9.08 μs       37.43 μs
with_map_new                89.98 K       11.11 μs   ±154.88%        8.94 μs       41.93 μs
with_enum_map_into          87.50 K       11.43 μs   ±168.60%        9.46 μs       30.92 μs
with_reduce_map_put         84.31 K       11.86 μs    ±63.69%       10.38 μs       38.56 μs
with_reduce_map_merge       84.29 K       11.86 μs    ±91.14%       10.25 μs       38.49 μs
with_for                    61.08 K       16.37 μs    ±95.14%       14.18 μs       36.76 μs

Comparison: 
with_enum_map_map_new       96.55 K
with_map_new                89.98 K - 1.07x slower +0.76 μs
with_enum_map_into          87.50 K - 1.10x slower +1.07 μs
with_reduce_map_put         84.31 K - 1.15x slower +1.50 μs
with_reduce_map_merge       84.29 K - 1.15x slower +1.51 μs
with_for                    61.08 K - 1.58x slower +6.01 μs

在我的机器上运行基准测试时,这个顺序是一致的(因此自己运行很重要),for 每次都排在最后,将Enum.map 输送到Map.new 始终是最快的,其次是仅使用带有映射功能的Map.new。我确实坚持我最初的观点,即 Elixir 中的 for 主要用于理解,但它在语法上确实非常好。所有这些都是不错的选择,真的,它只是表明有几种方法可以实现相同的目标,这通常是 Elixir 的情况,所以,有时归结为偏好以及优化是否对你的目标至关重要正在做。

【讨论】:

  • 您的功能不等价。 with_enum 返回一个列表 [a: 1, b: 4, c: 9, d: 16],而 with_for 返回一个映射 %{a: 1, b: 4, c: 9, d: 16}。您需要将Enum.map 的返回通过管道传输到Enum.into(%{})。当您添加该步骤时,for 执行操作的速度提高了 ~1.3 倍。 OP 正在专门寻找返回修改后的地图,所以这个答案是不正确的。还有一种方法可以使用Enum.reduce 做到这一点,它比Enum.map |&gt; Enum.into 更快。
  • 你是对的@m.simonborg,谢谢你指出这一点。我已经更新了我的答案,并包括了 forEnum.map() |&gt; Map.new() 的其他替代方案,只是为了让您了解更多的选择。
【解决方案2】:

@sbacarob 的回答是正确的想法,但缺少一些重要的东西,因此是不正确的。 with_enum 函数缺少管道到 Enum.into(%{}) 以返回映射的关键步骤,而是返回一个列表。这两个函数的返回值不相等,使得基准比较没有意义。更好的测试如下所示:

in "test.exs"

m = %{a: 1, b: 2, c: 3, d: 4}

with_enum = fn ->
  Enum.map(m, fn {k, v} -> {k, v * v} end) |> Enum.into(%{})
end

with_for = fn ->
  for {k, v} <- m, into: %{}, do: {k, v * v}
end

Benchee.run(%{
  "with_for" => with_for,
  "with_enum" => with_enum
})
in iex with Benchee available

iex> c "test.exs"
...
Benchmarking with_enum...
Benchmarking with_for...

Name                ips        average  deviation         median         99th %
with_for         1.82 M      548.27 ns  ±7120.96%           0 ns        1000 ns
with_enum        1.10 M      911.30 ns  ±2744.21%           0 ns        2000 ns

Comparison: 
with_for         1.82 M
with_enum        1.10 M - 1.66x slower +363.04 ns
...

具体结果因运行而异,但with_for 在我的测试中始终快 > 1.3 倍。我认为Enum 的另一个选项客观上比map 更好,无论是在速度上还是因为它是为此目的而构建的:Enum.reduce

in test.exs

m = %{a: 1, b: 2, c: 3, d: 4}

with_enum = fn ->
  Enum.map(m, fn {k, v} -> {k, v * v} end) |> Enum.into(%{})
end

with_for = fn ->
  for {k, v} <- m, into: %{}, do: {k, v * v}
end

with_reduce = fn ->
  Enum.reduce(m, %{}, fn {k, v}, new ->  Map.put(new, k, v) end)
end

Benchee.run(%{
  "with_for" => with_for,
  "with_enum" => with_enum,
  "with_reduce" => with_reduce
})
in iex

iex> c "test.exs"
...
Benchmarking with_enum...
Benchmarking with_for...
Benchmarking with_reduce...

Name                  ips        average  deviation         median         99th %
with_reduce        2.01 M      497.03 ns  ±7883.91%           0 ns        1000 ns
with_for           1.88 M      531.75 ns  ±6135.44%           0 ns        1000 ns
with_enum          1.07 M      933.62 ns  ±2757.77%           0 ns        3000 ns

Comparison: 
with_reduce        2.01 M
with_for           1.88 M - 1.07x slower +34.72 ns
with_enum          1.07 M - 1.88x slower +436.59 ns
...

在我的测试中,with_reducewith_for 轮流比另一个慢 for,因为语法更具可读性和美观性。正如Getting Started guide 中所述,for 推导式是一种特殊的语法糖,专门添加到语言中,以使此类转换更清晰、更易于理解。

【讨论】:

    猜你喜欢
    • 2017-06-24
    • 2018-02-05
    • 1970-01-01
    • 1970-01-01
    • 2018-12-14
    • 2018-05-31
    • 1970-01-01
    • 2019-12-02
    • 2016-05-05
    相关资源
    最近更新 更多