Skip to content

Optimize Map JSON encoding using :maps.to_list - #15474

Closed
preciz wants to merge 1 commit into
elixir-lang:mainfrom
preciz:optimization10
Closed

Optimize Map JSON encoding using :maps.to_list#15474
preciz wants to merge 1 commit into
elixir-lang:mainfrom
preciz:optimization10

Conversation

@preciz

@preciz preciz commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Assisted-by: Antigravity CLI Gemini Flash 3.5

It seems like the upfront :maps.to_list version is ~10% faster than the :maps.iterator one.

Bench:

Mix.install([
  {:benchee, "~> 1.3"}
])

# Old Map Encoder using :maps.iterator and :maps.next
defmodule OldMapEncoder do
  def encode(value, encoder) do
    case :maps.next(:maps.iterator(value)) do
      :none ->
        "{}"

      {key, value, iterator} ->
        [?{, key(key, encoder), ?:, encoder.(value, encoder) | next(iterator, encoder)]
    end
  end

  defp next(iterator, encoder) do
    case :maps.next(iterator) do
      :none ->
        "}"

      {key, value, iterator} ->
        [?,, key(key, encoder), ?:, encoder.(value, encoder) | next(iterator, encoder)]
    end
  end

  defp key(key, encoder) when is_atom(key), do: encoder.(Atom.to_string(key), encoder)
  defp key(key, encoder) when is_binary(key), do: encoder.(key, encoder)
  defp key(key, encoder), do: encoder.(String.Chars.to_string(key), encoder)
end

# New Map Encoder using :maps.to_list
defmodule NewMapEncoder do
  def encode(value, encoder) do
    case :maps.to_list(value) do
      [] ->
        "{}"

      [{key, value} | rest] ->
        [?{, key(key, encoder), ?:, encoder.(value, encoder) | next(rest, encoder)]
    end
  end

  defp next([], _encoder) do
    "}"
  end

  defp next([{key, value} | rest], encoder) do
    [?,, key(key, encoder), ?:, encoder.(value, encoder) | next(rest, encoder)]
  end

  defp key(key, encoder) when is_atom(key), do: encoder.(Atom.to_string(key), encoder)
  defp key(key, encoder) when is_binary(key), do: encoder.(key, encoder)
  defp key(key, encoder), do: encoder.(String.Chars.to_string(key), encoder)
end

# Old full encoder dispatcher mimicking JSON.protocol_encode/2
defmodule OldEncoder do
  def encode(value, encoder) when is_atom(value) do
    case value do
      nil -> "null"
      true -> "true"
      false -> "false"
      _ -> encoder.(Atom.to_string(value), encoder)
    end
  end

  def encode(value, _encoder) when is_binary(value),
    do: :json.encode_binary(value)

  def encode(value, _encoder) when is_integer(value),
    do: :json.encode_integer(value)

  def encode(value, _encoder) when is_float(value),
    do: :json.encode_float(value)

  def encode(value, encoder) when is_list(value),
    do: :json.encode_list(value, encoder)

  def encode(%{} = value, encoder) when not is_map_key(value, :__struct__),
    do: OldMapEncoder.encode(value, encoder)

  def encode(value, encoder),
    do: JSON.Encoder.encode(value, encoder)
end

# New full encoder dispatcher mimicking JSON.protocol_encode/2
defmodule NewEncoder do
  def encode(value, encoder) when is_atom(value) do
    case value do
      nil -> "null"
      true -> "true"
      false -> "false"
      _ -> encoder.(Atom.to_string(value), encoder)
    end
  end

  def encode(value, _encoder) when is_binary(value),
    do: :json.encode_binary(value)

  def encode(value, _encoder) when is_integer(value),
    do: :json.encode_integer(value)

  def encode(value, _encoder) when is_float(value),
    do: :json.encode_float(value)

  def encode(value, encoder) when is_list(value),
    do: :json.encode_list(value, encoder)

  def encode(%{} = value, encoder) when not is_map_key(value, :__struct__),
    do: NewMapEncoder.encode(value, encoder)

  def encode(value, encoder),
    do: JSON.Encoder.encode(value, encoder)
end

# Benchmark inputs covering small, medium, large, and nested map structures.
inputs = %{
  "Small Map (5 Atom Keys)" => Map.new(1..5, fn i -> {String.to_atom("key_#{i}"), i} end),
  "Medium Map (100 Atom Keys)" => Map.new(1..100, fn i -> {String.to_atom("key_#{i}"), i} end),
  "Large Map (10,000 Atom Keys)" => Map.new(1..10000, fn i -> {String.to_atom("key_#{i}"), i} end),

  "Small Map (5 String Keys)" => Map.new(1..5, fn i -> {"key_#{i}", i} end),
  "Medium Map (100 String Keys)" => Map.new(1..100, fn i -> {"key_#{i}", i} end),
  "Large Map (10,000 String Keys)" => Map.new(1..10000, fn i -> {"key_#{i}", i} end),

  "Nested Map" => %{a: 1, b: %{c: 2, d: %{e: 3, f: %{g: 4}}}}
}

Benchee.run(
  %{
    "Old (:maps.iterator)" => fn input ->
      IO.iodata_to_binary(OldEncoder.encode(input, &OldEncoder.encode/2))
    end,
    "New (:maps.to_list)" => fn input ->
      IO.iodata_to_binary(NewEncoder.encode(input, &NewEncoder.encode/2))
    end
  },
  inputs: inputs,
  time: 2,
  memory_time: 2
)

Bench results (noisy system, heat throttling mini pc):

Operating System: Linux
CPU Information: AMD Ryzen 7 8845HS w
Number of Available Cores: 16
Available memory: 54.72 GB
Elixir 1.20.0
Erlang 29.0.1
JIT enabled: true

Benchmark suite executing with the following configuration:
warmup: 2 s
time: 2 s
memory time: 2 s
reduction time: 0 ns
parallel: 1
inputs: Large Map (10,000 Atom Keys), Large Map (10,000 String Keys), Medium Map (100 Atom Keys), Medium Map (100 String Keys), Nested Map, Small Map (5 Atom Keys), Small Map (5 String Keys)
Estimated total run time: 1 min 24 s
Excluding outliers: false

Benchmarking New (:maps.to_list) with input Large Map (10,000 Atom Keys) ...
Benchmarking New (:maps.to_list) with input Large Map (10,000 String Keys) ...
Benchmarking New (:maps.to_list) with input Medium Map (100 Atom Keys) ...
Benchmarking New (:maps.to_list) with input Medium Map (100 String Keys) ...
Benchmarking New (:maps.to_list) with input Nested Map ...
Benchmarking New (:maps.to_list) with input Small Map (5 Atom Keys) ...
Benchmarking New (:maps.to_list) with input Small Map (5 String Keys) ...
Benchmarking Old (:maps.iterator) with input Large Map (10,000 Atom Keys) ...
Benchmarking Old (:maps.iterator) with input Large Map (10,000 String Keys) ...
Benchmarking Old (:maps.iterator) with input Medium Map (100 Atom Keys) ...
Benchmarking Old (:maps.iterator) with input Medium Map (100 String Keys) ...
Benchmarking Old (:maps.iterator) with input Nested Map ...
Benchmarking Old (:maps.iterator) with input Small Map (5 Atom Keys) ...
Benchmarking Old (:maps.iterator) with input Small Map (5 String Keys) ...
Calculating statistics...
Formatting results...

##### With input Large Map (10,000 Atom Keys) #####
Name                           ips        average  deviation         median         99th %
New (:maps.to_list)         975.72        1.02 ms    ±11.73%        1.01 ms        1.60 ms
Old (:maps.iterator)        743.18        1.35 ms    ±17.25%        1.27 ms        2.48 ms

Comparison:
New (:maps.to_list)         975.72
Old (:maps.iterator)        743.18 - 1.31x slower +0.32 ms

Memory usage statistics:

Name                    Memory usage
New (:maps.to_list)          1.83 MB
Old (:maps.iterator)         1.81 MB - 0.99x memory usage -0.02509 MB

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

##### With input Large Map (10,000 String Keys) #####
Name                           ips        average  deviation         median         99th %
New (:maps.to_list)         1.25 K        0.80 ms     ±8.33%        0.79 ms        0.98 ms
Old (:maps.iterator)        0.91 K        1.10 ms    ±10.31%        1.08 ms        1.58 ms

Comparison:
New (:maps.to_list)         1.25 K
Old (:maps.iterator)        0.91 K - 1.37x slower +0.30 ms

Memory usage statistics:

Name                    Memory usage
New (:maps.to_list)          1.83 MB
Old (:maps.iterator)         1.81 MB - 0.99x memory usage -0.02109 MB

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

##### With input Medium Map (100 Atom Keys) #####
Name                           ips        average  deviation         median         99th %
New (:maps.to_list)       125.05 K        8.00 μs    ±29.14%        7.69 μs       11.67 μs
Old (:maps.iterator)      105.79 K        9.45 μs    ±24.70%        9.16 μs       13.14 μs

Comparison:
New (:maps.to_list)       125.05 K
Old (:maps.iterator)      105.79 K - 1.18x slower +1.46 μs

Memory usage statistics:

Name                    Memory usage
New (:maps.to_list)         16.95 KB
Old (:maps.iterator)        16.72 KB - 0.99x memory usage -0.22656 KB

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

##### With input Medium Map (100 String Keys) #####
Name                           ips        average  deviation         median         99th %
New (:maps.to_list)       141.51 K        7.07 μs    ±39.51%        6.84 μs       11.05 μs
Old (:maps.iterator)      117.21 K        8.53 μs    ±24.72%        8.28 μs       11.99 μs

Comparison:
New (:maps.to_list)       141.51 K
Old (:maps.iterator)      117.21 K - 1.21x slower +1.46 μs

Memory usage statistics:

Name                    Memory usage
New (:maps.to_list)         16.97 KB
Old (:maps.iterator)        18.82 KB - 1.11x memory usage +1.85 KB

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

##### With input Nested Map #####
Name                           ips        average  deviation         median         99th %
New (:maps.to_list)         1.90 M      525.59 ns  ±1003.06%         491 ns         731 ns
Old (:maps.iterator)        1.46 M      685.97 ns   ±872.83%         641 ns         922 ns

Comparison:
New (:maps.to_list)         1.90 M
Old (:maps.iterator)        1.46 M - 1.31x slower +160.38 ns

Memory usage statistics:

Name                    Memory usage
New (:maps.to_list)          1.36 KB
Old (:maps.iterator)         1.37 KB - 1.01x memory usage +0.00781 KB

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

##### With input Small Map (5 Atom Keys) #####
Name                           ips        average  deviation         median         99th %
New (:maps.to_list)         2.19 M      456.76 ns  ±1893.76%         410 ns         621 ns
Old (:maps.iterator)        1.91 M      522.26 ns  ±1662.34%         451 ns         792 ns

Comparison:
New (:maps.to_list)         2.19 M
Old (:maps.iterator)        1.91 M - 1.14x slower +65.50 ns

Memory usage statistics:

Name                    Memory usage
New (:maps.to_list)          1.05 KB
Old (:maps.iterator)         1.02 KB - 0.98x memory usage -0.02344 KB

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

##### With input Small Map (5 String Keys) #####
Name                           ips        average  deviation         median         99th %
New (:maps.to_list)         2.29 M      436.88 ns  ±2107.81%         381 ns         721 ns
Old (:maps.iterator)        2.05 M      486.63 ns  ±1807.10%         431 ns         681 ns

Comparison:
New (:maps.to_list)         2.29 M
Old (:maps.iterator)        2.05 M - 1.11x slower +49.75 ns

Memory usage statistics:

Name                    Memory usage
New (:maps.to_list)          1.05 KB
Old (:maps.iterator)         1.02 KB - 0.98x memory usage -0.02344 KB

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

@josevalim

Copy link
Copy Markdown
Member

The Erlang version uses a map comprehension, can you try writing an Erlang function in elixir_utils that does the same and measure against that too?

encode_map(Map, Encode) when is_map(Map) ->
    do_encode_map(Map, Encode).

do_encode_map(Map, Encode) when is_function(Encode, 2) ->
    encode_object([[$,, key(Key, Encode), $: | Encode(Value, Encode)] || Key := Value <- Map]).

@preciz

preciz commented Jun 14, 2026

Copy link
Copy Markdown
Contributor Author

New bench:

Mix.install([
  {:benchee, "~> 1.3"}
])

# 1. Dynamically compile the Erlang Map Comprehension implementation
erl_code = ~S"""
-module(erl_map_encoder).
-export([encode_map/2]).

encode_map(Map, Encode) when is_map(Map) ->
    do_encode_map(Map, Encode).

do_encode_map(Map, Encode) when is_function(Encode, 2) ->
    encode_object([[$,, key(Key, Encode), $: | Encode(Value, Encode)] || Key := Value <- Map]).

key(Key, Encode) when is_atom(Key) ->
    Encode(atom_to_binary(Key, utf8), Encode);
key(Key, Encode) when is_binary(Key) ->
    Encode(Key, Encode);
key(Key, Encode) ->
    Encode('Elixir.String.Chars':to_string(Key), Encode).

encode_object([]) ->
    <<"{}">>;
encode_object([[$, | First] | Rest]) ->
    [${, First, Rest | "}"].
"""

File.write!("erl_map_encoder.erl", erl_code)
{:ok, module, bin} = :compile.file(~c"erl_map_encoder.erl", [:binary])
File.rm!("erl_map_encoder.erl")
:code.load_binary(module, ~c"erl_map_encoder.erl", bin)

# 2. Old Map Encoder using :maps.iterator and :maps.next
defmodule OldMapEncoder do
  def encode(value, encoder) do
    case :maps.next(:maps.iterator(value)) do
      :none ->
        "{}"

      {key, value, iterator} ->
        [?{, key(key, encoder), ?:, encoder.(value, encoder) | next(iterator, encoder)]
    end
  end

  defp next(iterator, encoder) do
    case :maps.next(iterator) do
      :none ->
        "}"

      {key, value, iterator} ->
        [?,, key(key, encoder), ?:, encoder.(value, encoder) | next(iterator, encoder)]
    end
  end

  defp key(key, encoder) when is_atom(key), do: encoder.(Atom.to_string(key), encoder)
  defp key(key, encoder) when is_binary(key), do: encoder.(key, encoder)
  defp key(key, encoder), do: encoder.(String.Chars.to_string(key), encoder)
end

# 3. New Map Encoder using :maps.to_list
defmodule NewMapEncoder do
  def encode(value, encoder) do
    case :maps.to_list(value) do
      [] ->
        "{}"

      [{key, value} | rest] ->
        [?{, key(key, encoder), ?:, encoder.(value, encoder) | next(rest, encoder)]
    end
  end

  defp next([], _encoder) do
    "}"
  end

  defp next([{key, value} | rest], encoder) do
    [?,, key(key, encoder), ?:, encoder.(value, encoder) | next(rest, encoder)]
  end

  defp key(key, encoder) when is_atom(key), do: encoder.(Atom.to_string(key), encoder)
  defp key(key, encoder) when is_binary(key), do: encoder.(key, encoder)
  defp key(key, encoder), do: encoder.(String.Chars.to_string(key), encoder)
end

# Old full encoder dispatcher
defmodule OldEncoder do
  def encode(value, encoder) when is_atom(value) do
    case value do
      nil -> "null"
      true -> "true"
      false -> "false"
      _ -> encoder.(Atom.to_string(value), encoder)
    end
  end

  def encode(value, _encoder) when is_binary(value),
    do: :json.encode_binary(value)

  def encode(value, _encoder) when is_integer(value),
    do: :json.encode_integer(value)

  def encode(value, _encoder) when is_float(value),
    do: :json.encode_float(value)

  def encode(value, encoder) when is_list(value),
    do: :json.encode_list(value, encoder)

  def encode(%{} = value, encoder) when not is_map_key(value, :__struct__),
    do: OldMapEncoder.encode(value, encoder)

  def encode(value, encoder),
    do: JSON.Encoder.encode(value, encoder)
end

# New full encoder dispatcher
defmodule NewEncoder do
  def encode(value, encoder) when is_atom(value) do
    case value do
      nil -> "null"
      true -> "true"
      false -> "false"
      _ -> encoder.(Atom.to_string(value), encoder)
    end
  end

  def encode(value, _encoder) when is_binary(value),
    do: :json.encode_binary(value)

  def encode(value, _encoder) when is_integer(value),
    do: :json.encode_integer(value)

  def encode(value, _encoder) when is_float(value),
    do: :json.encode_float(value)

  def encode(value, encoder) when is_list(value),
    do: :json.encode_list(value, encoder)

  def encode(%{} = value, encoder) when not is_map_key(value, :__struct__),
    do: NewMapEncoder.encode(value, encoder)

  def encode(value, encoder),
    do: JSON.Encoder.encode(value, encoder)
end

# Erlang Map Comprehension full encoder dispatcher
defmodule ErlangComprehensionEncoder do
  def encode(value, encoder) when is_atom(value) do
    case value do
      nil -> "null"
      true -> "true"
      false -> "false"
      _ -> encoder.(Atom.to_string(value), encoder)
    end
  end

  def encode(value, _encoder) when is_binary(value),
    do: :json.encode_binary(value)

  def encode(value, _encoder) when is_integer(value),
    do: :json.encode_integer(value)

  def encode(value, _encoder) when is_float(value),
    do: :json.encode_float(value)

  def encode(value, encoder) when is_list(value),
    do: :json.encode_list(value, encoder)

  def encode(%{} = value, encoder) when not is_map_key(value, :__struct__),
    do: :erl_map_encoder.encode_map(value, encoder)

  def encode(value, encoder),
    do: JSON.Encoder.encode(value, encoder)
end

# Benchmark inputs
inputs = %{
  "Small Map (5 Atom Keys)" => Map.new(1..5, fn i -> {String.to_atom("key_#{i}"), i} end),
  "Medium Map (100 Atom Keys)" => Map.new(1..100, fn i -> {String.to_atom("key_#{i}"), i} end),
  "Large Map (10,000 Atom Keys)" => Map.new(1..10000, fn i -> {String.to_atom("key_#{i}"), i} end),
  
  "Small Map (5 String Keys)" => Map.new(1..5, fn i -> {"key_#{i}", i} end),
  "Medium Map (100 String Keys)" => Map.new(1..100, fn i -> {"key_#{i}", i} end),
  "Large Map (10,000 String Keys)" => Map.new(1..10000, fn i -> {"key_#{i}", i} end),
  
  "Nested Map" => %{a: 1, b: %{c: 2, d: %{e: 3, f: %{g: 4}}}}
}

# Verification step to ensure correctness of all implementations
for {name, input} <- inputs do
  old_out = IO.iodata_to_binary(OldEncoder.encode(input, &OldEncoder.encode/2))
  new_out = IO.iodata_to_binary(NewEncoder.encode(input, &NewEncoder.encode/2))
  erl_out = IO.iodata_to_binary(ErlangComprehensionEncoder.encode(input, &ErlangComprehensionEncoder.encode/2))

  if old_out != new_out or new_out != erl_out do
    raise "Mismatch for input #{name}:\nOld: #{inspect(old_out)}\nNew: #{inspect(new_out)}\nErl: #{inspect(erl_out)}"
  end
end

IO.puts("All implementations verified successfully! Running benchmark...\n")

Benchee.run(
  %{
    "Old Elixir (:maps.iterator)" => fn input ->
      IO.iodata_to_binary(OldEncoder.encode(input, &OldEncoder.encode/2))
    end,
    "New Elixir (:maps.to_list)" => fn input ->
      IO.iodata_to_binary(NewEncoder.encode(input, &NewEncoder.encode/2))
    end,
    "Erlang (Map Comprehension)" => fn input ->
      IO.iodata_to_binary(ErlangComprehensionEncoder.encode(input, &ErlangComprehensionEncoder.encode/2))
    end
  },
  inputs: inputs,
  time: 2,
  memory_time: 2
)

Results on my noisy heat throttling machine:

All implementations verified successfully! Running benchmark...

Operating System: Linux
CPU Information: AMD Ryzen 7 8845HS w
Number of Available Cores: 16
Available memory: 54.72 GB
Elixir 1.20.0
Erlang 29.0.1
JIT enabled: true

Benchmark suite executing with the following configuration:
warmup: 2 s
time: 2 s
memory time: 2 s
reduction time: 0 ns
parallel: 1
inputs: Large Map (10,000 Atom Keys), Large Map (10,000 String Keys), Medium Map (100 Atom Keys), Medium Map (100 String Keys), Nested Map, Small Map (5 Atom Keys), Small Map (5 String Keys)
Estimated total run time: 2 min 6 s
Excluding outliers: false

##### With input Large Map (10,000 Atom Keys) #####
Name                                  ips        average  deviation         median         99th %
New Elixir (:maps.to_list)         793.94        1.26 ms    ±27.60%        1.14 ms        2.89 ms
Old Elixir (:maps.iterator)        747.66        1.34 ms    ±16.42%        1.29 ms        2.52 ms
Erlang (Map Comprehension)         731.36        1.37 ms    ±18.67%        1.30 ms        2.81 ms

Comparison:
New Elixir (:maps.to_list)         793.94
Old Elixir (:maps.iterator)        747.66 - 1.06x slower +0.0780 ms
Erlang (Map Comprehension)         731.36 - 1.09x slower +0.108 ms

Memory usage statistics:

Name                           Memory usage
New Elixir (:maps.to_list)          1.83 MB
Old Elixir (:maps.iterator)         1.78 MB - 0.97x memory usage -0.05125 MB
Erlang (Map Comprehension)          1.80 MB - 0.98x memory usage -0.02953 MB

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

##### With input Large Map (10,000 String Keys) #####
Name                                  ips        average  deviation         median         99th %
New Elixir (:maps.to_list)        1146.88        0.87 ms    ±13.31%        0.85 ms        1.47 ms
Old Elixir (:maps.iterator)        946.18        1.06 ms    ±12.81%        1.04 ms        1.58 ms
Erlang (Map Comprehension)         919.07        1.09 ms    ±11.07%        1.07 ms        1.73 ms

Comparison:
New Elixir (:maps.to_list)        1146.88
Old Elixir (:maps.iterator)        946.18 - 1.21x slower +0.185 ms
Erlang (Map Comprehension)         919.07 - 1.25x slower +0.22 ms

Memory usage statistics:

Name                           Memory usage
New Elixir (:maps.to_list)          1.83 MB
Old Elixir (:maps.iterator)         1.81 MB - 0.99x memory usage -0.02109 MB
Erlang (Map Comprehension)          1.79 MB - 0.98x memory usage -0.03703 MB

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

##### With input Medium Map (100 Atom Keys) #####
Name                                  ips        average  deviation         median         99th %
New Elixir (:maps.to_list)       116.71 K        8.57 μs    ±31.95%        8.15 μs       14.49 μs
Old Elixir (:maps.iterator)      101.52 K        9.85 μs    ±25.13%        9.52 μs       13.86 μs
Erlang (Map Comprehension)        96.50 K       10.36 μs    ±42.69%        9.96 μs       15.33 μs

Comparison:
New Elixir (:maps.to_list)       116.71 K
Old Elixir (:maps.iterator)      101.52 K - 1.15x slower +1.28 μs
Erlang (Map Comprehension)        96.50 K - 1.21x slower +1.80 μs

Memory usage statistics:

Name                           Memory usage
New Elixir (:maps.to_list)         16.95 KB
Old Elixir (:maps.iterator)        16.72 KB - 0.99x memory usage -0.22656 KB
Erlang (Map Comprehension)         16.75 KB - 0.99x memory usage -0.19531 KB

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

##### With input Medium Map (100 String Keys) #####
Name                                  ips        average  deviation         median         99th %
New Elixir (:maps.to_list)       132.44 K        7.55 μs    ±49.79%        7.21 μs       13.03 μs
Old Elixir (:maps.iterator)      113.07 K        8.84 μs    ±28.65%        8.55 μs       12.56 μs
Erlang (Map Comprehension)       103.94 K        9.62 μs    ±64.37%        9.12 μs       15.47 μs

Comparison:
New Elixir (:maps.to_list)       132.44 K
Old Elixir (:maps.iterator)      113.07 K - 1.17x slower +1.29 μs
Erlang (Map Comprehension)       103.94 K - 1.27x slower +2.07 μs

Memory usage statistics:

Name                           Memory usage
New Elixir (:maps.to_list)         16.97 KB
Old Elixir (:maps.iterator)        18.82 KB - 1.11x memory usage +1.85 KB
Erlang (Map Comprehension)         18.85 KB - 1.11x memory usage +1.88 KB

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

##### With input Nested Map #####
Name                                  ips        average  deviation         median         99th %
New Elixir (:maps.to_list)         1.83 M      546.34 ns  ±1071.97%         501 ns         962 ns
Old Elixir (:maps.iterator)        1.50 M      665.79 ns   ±836.30%         611 ns         992 ns
Erlang (Map Comprehension)         1.41 M      709.49 ns   ±661.54%         671 ns        1172 ns

Comparison:
New Elixir (:maps.to_list)         1.83 M
Old Elixir (:maps.iterator)        1.50 M - 1.22x slower +119.45 ns
Erlang (Map Comprehension)         1.41 M - 1.30x slower +163.15 ns

Memory usage statistics:

Name                           Memory usage
New Elixir (:maps.to_list)          1.36 KB
Old Elixir (:maps.iterator)         1.37 KB - 1.01x memory usage +0.00781 KB
Erlang (Map Comprehension)          1.49 KB - 1.10x memory usage +0.133 KB

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

##### With input Small Map (5 Atom Keys) #####
Name                                  ips        average  deviation         median         99th %
Erlang (Map Comprehension)         1.91 M      522.59 ns  ±1111.75%         481 ns         862 ns
Old Elixir (:maps.iterator)        1.88 M      532.44 ns  ±1789.13%         461 ns         802 ns
New Elixir (:maps.to_list)         1.23 M      813.15 ns  ±1821.37%         671 ns        2134 ns

Comparison:
Erlang (Map Comprehension)         1.91 M
Old Elixir (:maps.iterator)        1.88 M - 1.02x slower +9.85 ns
New Elixir (:maps.to_list)         1.23 M - 1.56x slower +290.56 ns

Memory usage statistics:

Name                           Memory usage
Erlang (Map Comprehension)          1.05 KB
Old Elixir (:maps.iterator)         1.02 KB - 0.97x memory usage -0.03125 KB
New Elixir (:maps.to_list)          1.05 KB - 0.99x memory usage -0.00781 KB

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

##### With input Small Map (5 String Keys) #####
Name                                  ips        average  deviation         median         99th %
New Elixir (:maps.to_list)         2.27 M      440.94 ns  ±1990.13%         391 ns         631 ns
Old Elixir (:maps.iterator)        1.99 M      501.32 ns  ±1854.81%         431 ns         782 ns
Erlang (Map Comprehension)         1.99 M      501.55 ns  ±1499.63%         451 ns         822 ns

Comparison:
New Elixir (:maps.to_list)         2.27 M
Old Elixir (:maps.iterator)        1.99 M - 1.14x slower +60.38 ns
Erlang (Map Comprehension)         1.99 M - 1.14x slower +60.61 ns

Memory usage statistics:

Name                           Memory usage
New Elixir (:maps.to_list)          1.05 KB
Old Elixir (:maps.iterator)         1.02 KB - 0.98x memory usage -0.02344 KB
Erlang (Map Comprehension)          1.05 KB - 1.01x memory usage +0.00781 KB

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

@josevalim

Copy link
Copy Markdown
Member

Any thoughts @michalmuskala? I would expect the other ones to be faster. :)

@preciz

preciz commented Jun 14, 2026

Copy link
Copy Markdown
Contributor Author

Yes this has to be measured by others. My PC can't be trusted, however for me it's consistently faster.

@AlexGx

AlexGx commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Results on my machine

Operating System: macOS
CPU Information: Apple M4 Max
Number of Available Cores: 16
Available memory: 48 GB
Elixir 1.20.1
Erlang 29.0.2
JIT enabled: true

Benchmark suite executing with the following configuration:
warmup: 2 s
time: 2 s
memory time: 2 s
reduction time: 0 ns
parallel: 1
inputs: Large Map (10,000 Atom Keys), Large Map (10,000 String Keys), Medium Map (100 Atom Keys), Medium Map (100 String Keys), Nested Map, Small Map (5 Atom Keys), Small Map (5 String Keys)
Estimated total run time: 2 min 6 s
Excluding outliers: false

##### With input Large Map (10,000 Atom Keys) #####
Name                                  ips        average  deviation         median         99th %
Erlang (Map Comprehension)         1.48 K      675.25 μs     ±5.34%      669.54 μs      939.28 μs
Old Elixir (:maps.iterator)        1.46 K      685.20 μs    ±41.93%      669.75 μs      876.26 μs
New Elixir (:maps.to_list)         1.43 K      699.80 μs    ±41.23%      691.17 μs      780.70 μs

Comparison: 
Erlang (Map Comprehension)         1.48 K
Old Elixir (:maps.iterator)        1.46 K - 1.01x slower +9.96 μs
New Elixir (:maps.to_list)         1.43 K - 1.04x slower +24.55 μs

Memory usage statistics:

Name                           Memory usage
Erlang (Map Comprehension)          1.83 MB
Old Elixir (:maps.iterator)         1.81 MB - 0.99x memory usage -0.01826 MB
New Elixir (:maps.to_list)          1.83 MB - 1.00x memory usage +0.00636 MB

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

##### With input Large Map (10,000 String Keys) #####
Name                                  ips        average  deviation         median         99th %
Old Elixir (:maps.iterator)        1.77 K      563.88 μs    ±68.53%      552.38 μs      643.40 μs
Erlang (Map Comprehension)         1.74 K      575.14 μs     ±2.66%      571.79 μs      633.59 μs
New Elixir (:maps.to_list)         1.70 K      589.60 μs    ±55.31%      580.21 μs      664.88 μs

Comparison: 
Old Elixir (:maps.iterator)        1.77 K
Erlang (Map Comprehension)         1.74 K - 1.02x slower +11.26 μs
New Elixir (:maps.to_list)         1.70 K - 1.05x slower +25.72 μs

Memory usage statistics:

Name                           Memory usage
Old Elixir (:maps.iterator)         1.79 MB
Erlang (Map Comprehension)          1.76 MB - 0.98x memory usage -0.02968 MB
New Elixir (:maps.to_list)          1.84 MB - 1.03x memory usage +0.0454 MB

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

##### With input Medium Map (100 Atom Keys) #####
Name                                  ips        average  deviation         median         99th %
Old Elixir (:maps.iterator)      171.49 K        5.83 μs    ±20.92%        5.75 μs        8.21 μs
Erlang (Map Comprehension)       171.39 K        5.83 μs    ±38.38%        5.54 μs       11.21 μs
New Elixir (:maps.to_list)       167.13 K        5.98 μs    ±43.14%        5.88 μs        8.54 μs

Comparison: 
Old Elixir (:maps.iterator)      171.49 K
Erlang (Map Comprehension)       171.39 K - 1.00x slower +0.00326 μs
New Elixir (:maps.to_list)       167.13 K - 1.03x slower +0.152 μs

Memory usage statistics:

Name                           Memory usage
Old Elixir (:maps.iterator)        18.28 KB
Erlang (Map Comprehension)         18.31 KB - 1.00x memory usage +0.0313 KB
New Elixir (:maps.to_list)         18.53 KB - 1.01x memory usage +0.25 KB

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

##### With input Medium Map (100 String Keys) #####
Name                                  ips        average  deviation         median         99th %
New Elixir (:maps.to_list)       175.35 K        5.70 μs    ±32.24%        5.63 μs        8.54 μs
Old Elixir (:maps.iterator)      175.34 K        5.70 μs   ±117.93%        5.58 μs           8 μs
Erlang (Map Comprehension)       170.31 K        5.87 μs    ±36.83%        5.58 μs       10.88 μs

Comparison: 
New Elixir (:maps.to_list)       175.35 K
Old Elixir (:maps.iterator)      175.34 K - 1.00x slower +0.00036 μs
Erlang (Map Comprehension)       170.31 K - 1.03x slower +0.169 μs

Memory usage statistics:

Name                           Memory usage
New Elixir (:maps.to_list)         19.94 KB
Old Elixir (:maps.iterator)        19.41 KB - 0.97x memory usage -0.53125 KB
Erlang (Map Comprehension)         19.44 KB - 0.97x memory usage -0.50000 KB

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

##### With input Nested Map #####
Name                                  ips        average  deviation         median         99th %
Erlang (Map Comprehension)         1.56 M      643.03 ns   ±639.50%         584 ns         833 ns
New Elixir (:maps.to_list)         1.55 M      646.23 ns   ±757.10%         625 ns         834 ns
Old Elixir (:maps.iterator)        1.52 M      658.86 ns   ±753.43%         625 ns         834 ns

Comparison: 
Erlang (Map Comprehension)         1.56 M
New Elixir (:maps.to_list)         1.55 M - 1.00x slower +3.20 ns
Old Elixir (:maps.iterator)        1.52 M - 1.02x slower +15.84 ns

Memory usage statistics:

Name                           Memory usage
Erlang (Map Comprehension)          3.02 KB
New Elixir (:maps.to_list)          2.92 KB - 0.97x memory usage -0.10156 KB
Old Elixir (:maps.iterator)         2.93 KB - 0.97x memory usage -0.09375 KB

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

##### With input Small Map (5 Atom Keys) #####
Name                                  ips        average  deviation         median         99th %
New Elixir (:maps.to_list)         1.69 M      592.87 ns   ±913.29%         542 ns         750 ns
Erlang (Map Comprehension)         1.69 M      593.12 ns   ±607.55%         542 ns         792 ns
Old Elixir (:maps.iterator)        1.66 M      601.76 ns   ±646.19%         583 ns         751 ns

Comparison: 
New Elixir (:maps.to_list)         1.69 M
Erlang (Map Comprehension)         1.69 M - 1.00x slower +0.25 ns
Old Elixir (:maps.iterator)        1.66 M - 1.01x slower +8.89 ns

Memory usage statistics:

Name                           Memory usage
New Elixir (:maps.to_list)          2.59 KB
Erlang (Map Comprehension)          2.62 KB - 1.01x memory usage +0.0313 KB
Old Elixir (:maps.iterator)         2.59 KB - 1.00x memory usage +0 KB

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

##### With input Small Map (5 String Keys) #####
Name                                  ips        average  deviation         median         99th %
Old Elixir (:maps.iterator)        1.72 M      580.88 ns   ±800.94%         542 ns         709 ns
Erlang (Map Comprehension)         1.71 M      584.71 ns   ±896.57%         542 ns         750 ns
New Elixir (:maps.to_list)         1.67 M      599.09 ns  ±1996.03%         542 ns         750 ns

Comparison: 
Old Elixir (:maps.iterator)        1.72 M
Erlang (Map Comprehension)         1.71 M - 1.01x slower +3.83 ns
New Elixir (:maps.to_list)         1.67 M - 1.03x slower +18.21 ns

Memory usage statistics:

Name                           Memory usage
Old Elixir (:maps.iterator)         2.56 KB
Erlang (Map Comprehension)          2.59 KB - 1.01x memory usage +0.0313 KB
New Elixir (:maps.to_list)          2.61 KB - 1.02x memory usage +0.0469 KB

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

@josevalim

Copy link
Copy Markdown
Member

Yeah, those are too close to show we should go one way or the other. So we may err on the side of keeping things as is.

@preciz

preciz commented Jun 15, 2026

Copy link
Copy Markdown
Contributor Author

Thank you all for the feedback

@preciz preciz closed this Jun 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants