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
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With