At which point should I use Map.merge?

Background

I have a map that I need to add some field to. For now I am using Map.put because I think it’s fast and simple:

base_map
|> Map.put("deal_id", deal_id)
|> Map.put("seat", seat)
|> Map.put("amount", some_amount)
|> Dispatcher.send()

Problem

The problem with this approach is that it grows into a monster very quickly. Imagine that instead of adding 3 parameters I add 6 or 7.

My solution to this problem is to use Map.merge:

missing_params = %{
  "deal_id" => deal_id 
  "seat" =>  seat
  "amount" => some_amount
}  

base_map
|> Map.merge(missing_params)
|> Dispatcher.send()

Questions:

Now my code is more readable, but I have some questions:

  1. Isn’t Map.merge considerably more heavy than Map.put?
  2. At which point is using Map.merge more efficient than doing Map.put repeatedly?
  3. Does it even matter (from an efficiency stand point)?
  4. Which approach would you use and why?
2 Likes

The compiler transforms your Map.put into a Map.merge and then transform the Map.merge into a VM operation. So both should be quite fast… so choose which ones reads better because…

…very unlikely. :slight_smile:

7 Likes

If I recall correctly, then Map.merge/2 calls :maps.merge/2, which as far as I know even is treated specially on the BEAM and not create temporaray values, while many Map.put/2 will generate many intermediate maps and therefore consume more memory and time.

4 Likes

Yep, I remember the talk about that optimization pass on the OTP repo if I recall correctly…

Let’s test it!! ^.^

Given this test:

defmodule MapMergePutBench do
  @classifiers [2, 10, 100, 1000]
  def classifiers(), do: @classifiers

  def time(_), do: 2

  def inputs(cla),
    do: %{
      "Orig:0" => %{},
      "Orig:1" => %{},
      "Orig:#{cla}" => Map.new(Enum.map(1..cla, &{&1, &1})),
    }

  def setup(_cla), do: nil

  def teardown(_cla, _setup), do: nil

  for cla <- @classifiers do
    def actions(unquote(cla), _setup),
      do: %{
        "Map.merge" => fn inp -> inp |> Map.merge(unquote({:%{}, [], Enum.map(1..cla, &{&1, &1+1})})) end,
        "Map.put" => fn inp -> inp |> unquote(Enum.reduce(
          2..cla,
          {{:., [], [{:__aliases__, [alias: false], [:Map]}, :put]}, [], [1, 2]},
          &{:|>, [context: Elixir, import: Kernel], [&2, {{:., [], [{:__aliases__, [alias: false], [:Map]}, :put]}, [], [&1, &1+1]}]}
        )) end,
      }
  end
end

Which should properly inline the merge and put calls with inlined data as it is the usual optimal condition (as in the OP in this thread), let’s test it at a variety of sizes!
Raw results:

╰─➤  mix bench map_merge_put 

Benchmarking Classifier: 2
==========================

Operating System: Linux"
CPU Information: AMD Phenom(tm) II X6 1090T Processor
Number of Available Cores: 6
Available memory: 15.67 GB
Elixir 1.8.0-rc.1
Erlang 21.2.2

Benchmark suite executing with the following configuration:
warmup: 2 s
time: 2 s
memory time: 2 s
parallel: 1
inputs: Orig:0, Orig:1, Orig:2
Estimated total run time: 36 s


Benchmarking Map.merge with input Orig:0...
Benchmarking Map.merge with input Orig:1...
Benchmarking Map.merge with input Orig:2...
Benchmarking Map.put with input Orig:0...
Benchmarking Map.put with input Orig:1...
Benchmarking Map.put with input Orig:2...

##### With input Orig:0 #####
Name                ips        average  deviation         median         99th %
Map.put         15.11 M      0.0662 μs   ±598.21%      0.0600 μs       0.120 μs
Map.merge       15.09 M      0.0663 μs   ±487.88%      0.0600 μs       0.140 μs

Comparison: 
Map.put         15.11 M
Map.merge       15.09 M - 1.00x slower

Memory usage statistics:

Name         Memory usage
Map.put             200 B
Map.merge           200 B - 1.00x memory usage

**All measurements for memory usage were the same**

##### With input Orig:1 #####
Name                ips        average  deviation         median         99th %
Map.merge       15.12 M      0.0661 μs   ±551.74%      0.0600 μs       0.140 μs
Map.put         15.04 M      0.0665 μs   ±458.40%      0.0600 μs       0.140 μs

Comparison: 
Map.merge       15.12 M
Map.put         15.04 M - 1.01x slower

Memory usage statistics:

Name         Memory usage
Map.merge           200 B
Map.put             200 B - 1.00x memory usage

**All measurements for memory usage were the same**

##### With input Orig:2 #####
Name                ips        average  deviation         median         99th %
Map.put         14.51 M      0.0689 μs   ±460.88%      0.0600 μs       0.170 μs
Map.merge       13.79 M      0.0725 μs   ±509.28%      0.0700 μs       0.150 μs

Comparison: 
Map.put         14.51 M
Map.merge       13.79 M - 1.05x slower

Memory usage statistics:

Name         Memory usage
Map.put             280 B
Map.merge           280 B - 1.00x memory usage

**All measurements for memory usage were the same**

Benchmarking Classifier: 10
===========================

Operating System: Linux"
CPU Information: AMD Phenom(tm) II X6 1090T Processor
Number of Available Cores: 6
Available memory: 15.67 GB
Elixir 1.8.0-rc.1
Erlang 21.2.2

Benchmark suite executing with the following configuration:
warmup: 2 s
time: 2 s
memory time: 2 s
parallel: 1
inputs: Orig:0, Orig:1, Orig:10
Estimated total run time: 36 s


Benchmarking Map.merge with input Orig:0...
Benchmarking Map.merge with input Orig:1...
Benchmarking Map.merge with input Orig:10...
Benchmarking Map.put with input Orig:0...
Benchmarking Map.put with input Orig:1...
Benchmarking Map.put with input Orig:10...

##### With input Orig:0 #####
Name                ips        average  deviation         median         99th %
Map.put         11.24 M      0.0890 μs   ±325.53%      0.0800 μs       0.180 μs
Map.merge       10.84 M      0.0922 μs   ±315.66%      0.0800 μs       0.190 μs

Comparison: 
Map.put         11.24 M
Map.merge       10.84 M - 1.04x slower

Memory usage statistics:

Name         Memory usage
Map.put             328 B
Map.merge           328 B - 1.00x memory usage

**All measurements for memory usage were the same**

##### With input Orig:1 #####
Name                ips        average  deviation         median         99th %
Map.put         11.21 M      0.0892 μs   ±329.04%      0.0800 μs       0.180 μs
Map.merge       10.71 M      0.0934 μs   ±329.74%      0.0800 μs        0.20 μs

Comparison: 
Map.put         11.21 M
Map.merge       10.71 M - 1.05x slower

Memory usage statistics:

Name         Memory usage
Map.put             328 B
Map.merge           328 B - 1.00x memory usage

**All measurements for memory usage were the same**

##### With input Orig:10 #####
Name                ips        average  deviation         median         99th %
Map.merge        6.36 M       0.157 μs  ±3498.41%       0.100 μs        0.50 μs
Map.put          6.27 M       0.160 μs  ±3496.35%       0.100 μs        0.50 μs

Comparison: 
Map.merge        6.36 M
Map.put          6.27 M - 1.02x slower

Memory usage statistics:

Name         Memory usage
Map.merge           600 B
Map.put             600 B - 1.00x memory usage

**All measurements for memory usage were the same**

Benchmarking Classifier: 100
============================

Operating System: Linux"
CPU Information: AMD Phenom(tm) II X6 1090T Processor
Number of Available Cores: 6
Available memory: 15.67 GB
Elixir 1.8.0-rc.1
Erlang 21.2.2

Benchmark suite executing with the following configuration:
warmup: 2 s
time: 2 s
memory time: 2 s
parallel: 1
inputs: Orig:0, Orig:1, Orig:100
Estimated total run time: 36 s


Benchmarking Map.merge with input Orig:0...
Benchmarking Map.merge with input Orig:1...
Benchmarking Map.merge with input Orig:100...
Benchmarking Map.put with input Orig:0...
Benchmarking Map.put with input Orig:1...
Benchmarking Map.put with input Orig:100...

##### With input Orig:0 #####
Name                ips        average  deviation         median         99th %
Map.merge       85.46 K       11.70 μs    ±13.20%          11 μs          16 μs
Map.put         84.42 K       11.85 μs    ±16.17%          11 μs          17 μs

Comparison: 
Map.merge       85.46 K
Map.put         84.42 K - 1.01x slower

Memory usage statistics:

Name         Memory usage
Map.merge         1.73 KB
Map.put           1.73 KB - 1.00x memory usage

**All measurements for memory usage were the same**

##### With input Orig:1 #####
Name                ips        average  deviation         median         99th %
Map.merge       84.65 K       11.81 μs    ±68.72%          12 μs          17 μs
Map.put         82.88 K       12.07 μs    ±19.01%          12 μs          19 μs

Comparison: 
Map.merge       84.65 K
Map.put         82.88 K - 1.02x slower

Memory usage statistics:

Name         Memory usage
Map.merge         1.73 KB
Map.put           1.73 KB - 1.00x memory usage

**All measurements for memory usage were the same**

##### With input Orig:100 #####
Name                ips        average  deviation         median         99th %
Map.merge      111.28 K        8.99 μs    ±28.99%           8 μs          17 μs
Map.put        107.92 K        9.27 μs    ±35.87%           8 μs          19 μs

Comparison: 
Map.merge      111.28 K
Map.put        107.92 K - 1.03x slower

Memory usage statistics:

Name         Memory usage
Map.merge         4.52 KB
Map.put           4.52 KB - 1.00x memory usage

**All measurements for memory usage were the same**

Benchmarking Classifier: 1000
=============================

Operating System: Linux"
CPU Information: AMD Phenom(tm) II X6 1090T Processor
Number of Available Cores: 6
Available memory: 15.67 GB
Elixir 1.8.0-rc.1
Erlang 21.2.2

Benchmark suite executing with the following configuration:
warmup: 2 s
time: 2 s
memory time: 2 s
parallel: 1
inputs: Orig:0, Orig:1, Orig:1000
Estimated total run time: 36 s


Benchmarking Map.merge with input Orig:0...
Benchmarking Map.merge with input Orig:1...
Benchmarking Map.merge with input Orig:1000...
Benchmarking Map.put with input Orig:0...
Benchmarking Map.put with input Orig:1...
Benchmarking Map.put with input Orig:1000...

##### With input Orig:0 #####
Name                ips        average  deviation         median         99th %
Map.put          6.63 K      150.83 μs     ±4.68%         148 μs         177 μs
Map.merge        6.43 K      155.56 μs     ±7.29%         150 μs         191 μs

Comparison: 
Map.put          6.63 K
Map.merge        6.43 K - 1.03x slower

Memory usage statistics:

Name         Memory usage
Map.put          20.24 KB
Map.merge        20.24 KB - 1.00x memory usage

**All measurements for memory usage were the same**

##### With input Orig:1 #####
Name                ips        average  deviation         median         99th %
Map.merge        6.64 K      150.53 μs     ±4.67%         148 μs         174 μs
Map.put          6.60 K      151.62 μs     ±7.92%         148 μs         178 μs

Comparison: 
Map.merge        6.64 K
Map.put          6.60 K - 1.01x slower

Memory usage statistics:

Name         Memory usage
Map.merge        20.24 KB
Map.put          20.24 KB - 1.00x memory usage

**All measurements for memory usage were the same**

##### With input Orig:1000 #####
Name                ips        average  deviation         median         99th %
Map.put          4.42 K      226.17 μs     ±6.91%         219 μs         284 μs
Map.merge        4.29 K      232.85 μs     ±8.26%         225 μs         306 μs

Comparison: 
Map.put          4.42 K
Map.merge        4.29 K - 1.03x slower

Memory usage statistics:

Name         Memory usage
Map.put          32.36 KB
Map.merge        32.36 KB - 1.00x memory usage

**All measurements for memory usage were the same**

So in summary:

Original Map Size put/merge Map Updates Ops/sec Difference Mem
0 put 2 15.11 M 1.00x 200 B
0 merge 2 15.09 M 1.00x 200 B
1 merge 2 15.12 M 1.00x 200 B
1 put 2 15.04 M 1.01x 200 B
2 put 2 14.51 M 1.00x 280 B
2 merge 2 13.79 M 1.05x 280 B
0 put 10 11.24 M 1.00x 328 B
0 merge 10 10.84 M 1.04x 328 B
1 put 10 11.21 M 1.00x 328 B
1 merge 10 10.71 M 1.05x 328 B
10 merge 10 6.36 M 1.00x 600 B
10 put 10 6.27 M 1.02x 600 B
0 merge 100 85.46 K 1.00x 1.73 K
0 put 100 84.42 K 1.01x 1.73 K
1 merge 100 84.65 K 1.00x 1.73 K
1 put 100 82.88 K 1.02x 1.73 K
100 merge 100 111.28 K 1.00x 4.52 K
100 put 100 107.92 K 1.03x 4.52 K
0 put 1000 6.63 K 1.00x 20.24 K
0 merge 1000 6.43 K 1.03x 20.24 K
1 merge 1000 6.64 K 1.00x 20.24 K
1 put 1000 6.60 K 1.01x 20.24 K
1000 put 1000 4.42 K 1.00x 32.36 K
1000 merge 1000 4.29 K 1.03x 32.36 K

In other words they are about identical, well within error bounds, just outright well within the error bounds (deviation was averaging around 5%, even with that they were almost identical as it was). And I’m sure checking the generated code will probably show the same beam disassembly as well. :slight_smile:

So yeah, use whichever is cleaner for your code.

5 Likes

It is actually done in Elixir. Erlang actually does not inline calls into operations, because you lose tracing, but in Elixir we are a bit more loose about inlining (which comes naturally becomes of macros and having to be built on top of Erlang), so we take more liberties on this area.

4 Likes

Ooo very cool then! I think I’m remember the optimizations that you or michal or so put in to OTP around map merging then. ^.^