Skip to content

Optimize Inspect.List.keyword?/1 by using Atom.to_string/1#15631

Merged
josevalim merged 2 commits into
elixir-lang:mainfrom
Ljzn:optimize-inspect-keyword
Jul 18, 2026
Merged

Optimize Inspect.List.keyword?/1 by using Atom.to_string/1#15631
josevalim merged 2 commits into
elixir-lang:mainfrom
Ljzn:optimize-inspect-keyword

Conversation

@Ljzn

@Ljzn Ljzn commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Optimize Inspect.List.keyword?/1 by replacing Atom.to_charlist/1 with Atom.to_string/1 + binary pattern matching.

Problem

Inspect.List.keyword?/1 walks all key-value pairs to check if any key is an Elixir module name (starts with "Elixir."). For each atom key, it calls Atom.to_charlist/1, which allocates a new list on the heap. For a map with 10,000 atom-keyed entries, this is 10,000 temporary allocations that are immediately discarded.

Fix

Use Atom.to_string/1 + match?("Elixir." <> _, ...) instead. Binaries are cheaper to allocate and the pattern match is done directly on the binary representation without intermediate list creation.

Benchmark (n=10)

Test Before (ms) After (ms) Speedup
keyword?/1 (10K keys) 0.62 0.48 1.3x
keyword?/1 (50K keys) 2.72 1.69 1.6x
inspect map (10K entries) 2.59 1.52 1.7x
inspect map (50K entries) 8.85 5.94 1.5x

Assisted-by: opencode:deepseek-v4-flash

Atom.to_charlist/1 allocates a new list per atom key when checking
if a keyword list contains module names. Atom.to_string/1 produces
a binary that can be pattern matched directly, avoiding O(n) temporary
allocations when inspecting large maps and keyword lists.

Benchmark (n=10):
  keyword?/1 (10K):  0.62ms -> 0.48ms (1.3x)
  keyword?/1 (50K):  2.72ms -> 1.69ms (1.6x)
  inspect map (10K): 2.59ms -> 1.52ms (1.7x)
  inspect map (50K): 8.85ms -> 5.94ms (1.5x)

Assisted-by: opencode:deepseek-v4-flash
@Ljzn

Ljzn commented Jul 18, 2026

Copy link
Copy Markdown
Contributor Author

bechmark

Code.require_file("../lib/elixir/test/elixir/test_helper.exs", __DIR__)

defmodule InspectBench do
  @kw_10k Enum.map(1..10_000, fn i -> {String.to_atom("key_#{i}"), i} end)

  @kw_50k Enum.map(1..50_000, fn i -> {String.to_atom("key_#{i}"), i} end)

  @map_10k Map.new(1..10_000, fn i -> {String.to_atom("key_#{i}"), i} end)

  @map_50k Map.new(1..50_000, fn i -> {String.to_atom("key_#{i}"), i} end)

  def run(n \\ 10) do
    IO.puts("\n=== Inspect Performance Benchmark (n=#{n}) ===\n")
    header()

    results = [
      measure("keyword?/1 (10K)", n, fn -> Inspect.List.keyword?(@kw_10k) end),
      measure("keyword?/1 (50K)", n, fn -> Inspect.List.keyword?(@kw_50k) end),
      measure("inspect kwlist (10K)", n, fn -> inspect(@kw_10k) end),
      measure("inspect kwlist (50K)", n, fn -> inspect(@kw_50k) end),
      measure("inspect map (10K)", n, fn -> inspect(@map_10k) end),
      measure("inspect map (50K)", n, fn -> inspect(@map_50k) end)
    ]

    IO.puts("\nDone.")
    results
  end

  defp header do
    IO.puts(String.pad_trailing("Test", 28) <>
            String.pad_trailing("Avg (ms)", 12) <>
            String.pad_trailing("Min (ms)", 12) <>
            String.pad_trailing("Max (ms)", 12))
    IO.puts(String.duplicate("─", 64))
  end

  defp measure(name, n, fun) do
    fun.()  # warmup

    times = for _ <- 1..n, do: :timer.tc(fun) |> elem(0)
    avg = Enum.sum(times) / n / 1000
    min = Enum.min(times) / 1000
    max = Enum.max(times) / 1000

    IO.puts(String.pad_trailing(name, 28) <>
            String.pad_trailing(Float.to_string(Float.round(avg, 2)), 12) <>
            String.pad_trailing(Float.to_string(Float.round(min, 2)), 12) <>
            String.pad_trailing(Float.to_string(Float.round(max, 2)), 12))

    {name, times}
  end
end

InspectBench.run()

Comment thread lib/elixir/lib/inspect.ex Outdated
@josevalim
josevalim merged commit 34abdd4 into elixir-lang:main Jul 18, 2026
15 checks passed
@josevalim

Copy link
Copy Markdown
Member

💚 💙 💜 💛 ❤️

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.

2 participants