Reputation: 1377
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
Upvotes: 3
Views: 2104
Reputation: 2212
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.
Upvotes: 7
Reputation: 2575
@sbacarob's answer is the right idea but is missing something important and therefore incorrect. The with_enum
function is missing the critical step of piping to Enum.into(%{})
to return a map, and is returning a list instead. The returns of the two functions are not equivalent rendering the benchmark comparison moot. A better test would look like this:
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
...
Exact results vary from run to run but with_for
has been consistently >1.3x faster in my tests. There's another option with Enum
that I would argue is objectively better for this operation than map
, both in speed and because it's purpose built for it: 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
...
In my tests with_reduce
and with_for
take turns being <1.1x slower than the other, so I'd consider it a tossup and say they're practically equivalent. In the end I'd make the decision to use for
because the syntax is more readable and aesthetically nicer. As stated in the Getting Started guide, for
comprehensions are a special syntactic sugar added to the language specifically to make transformations like this cleaner and easier to understand.
Upvotes: 0