Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Elixir Enum.map vs For comprehension

I have a map and I am modifying each element on it, I am confused which approach is better(faster) to do it with Enum.map and then Enum.into(%{}) or to use for comprehension like

for {key, value} <- my_map, into: %{} do
  {key, new_value}
end
like image 820
Tano Avatar asked Sep 09 '26 21:09

Tano


1 Answers

Original answer

You can use Benchee to run this kind of comparisons.

A simple Benchee test will show that Enum is faster for cases like this one.

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

In general, for isn't the best option for these cases in Elixir, it's best suited for list comprehensions, which it can do pretty fast and with an easy to read syntax.

Enum's functions are optimized to handle these scenarios which are more iterative, like what you would do with a for construct in other programming languages.


Edit

Even though the main intention of my original answer was to point to a framework that helps to run such kind of comparisons so that OP could try for themselves, as pointed out by another user, the example function using Enum.map wasn't producing the same result as the one with for. As pointed out by himself, adding Enum.into to the Enum.map call results in a sometimes longer running function. So here's an update, adding some more options that could have also been considered to produce the same result, with a benchmark.

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

When running the benchmarks on my machine this order was consistent (hence the important of running things by yourself), with for coming in last every time and piping Enum.map into Map.new was consistently the fastest, followed by using just Map.new with a mapping function. I do insist in my original point that for in Elixir is mainly used for comprehensions, but it certainly is really nice syntactically. All of them are good options, really, and it just shows that there are several ways to achieve the same thing, which is often the case with Elixir, so, sometimes it boils down to preference and whether or not optimization is crucial to what you're doing.

like image 127
sbacarob Avatar answered Sep 12 '26 19:09

sbacarob



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!