Skip to content

Macro.classify_atom - binary fast path - #15768

Open
dkuku wants to merge 3 commits into
elixir-lang:mainfrom
dkuku:dk_classify_atom_binary_fast_path
Open

Macro.classify_atom - binary fast path#15768
dkuku wants to merge 3 commits into
elixir-lang:mainfrom
dkuku:dk_classify_atom_binary_fast_path

Conversation

@dkuku

@dkuku dkuku commented Aug 18, 2026

Copy link
Copy Markdown
Contributor
  • Add a function to check if an atom is an identifier - it is very common but it was last fallback:
    :identifier - the atom can be used as a variable or local function call (as well as be an unquoted atom)
  • Convert alias check to binary match as the binary name is already available.
  • Additional regression tests.
ids = for i <- 1..500, do: String.to_atom("field_name_#{i}")
short_ids = for i <- 1..500, do: String.to_atom("k#{rem(i, 26) + 97}")
bangs = for i <- 1..500, do: String.to_atom("valid_#{i}?")
aliases = for i <- 1..500, do: String.to_atom("Elixir.My.App.Schema#{i}.Nested")
quoted = for i <- 1..500, do: String.to_atom("has spaces #{i}")
unicode = for i <- 1..500, do: String.to_atom("héllo_wörld_#{i}")
upcased = for i <- 1..500, do: String.to_atom("NotAnElixirAlias#{i}")

inputs = %{
  "500 identifiers (long)" => ids,
  "500 identifiers (short)" => short_ids,
  "500 identifiers with ?" => bangs,
  "500 aliases" => aliases,
  "500 quoted atoms (slow path)" => quoted,
  "500 unicode atoms (slow path)" => unicode,
  "500 uppercase non-aliases (slow path)" => upcased
}


##### With input 500 aliases #####
Name                                  ips        average  deviation         median         99th %
inspect_atom(:key, _) (new)       19.24 K       51.98 μs    ±11.91%       51.57 μs       57.00 μs
inspect_atom(:key, _) (old)       10.28 K       97.31 μs    ±14.45%       96.14 μs      106.65 μs

Comparison:
inspect_atom(:key, _) (new)       19.24 K
inspect_atom(:key, _) (old)       10.28 K - 1.87x slower +45.33 μs

Memory usage statistics:

Name                           Memory usage
inspect_atom(:key, _) (new)        61.68 KB
inspect_atom(:key, _) (old)       258.95 KB - 4.20x memory usage +197.27 KB

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

##### With input 500 identifiers (long) #####
Name                                  ips        average  deviation         median         99th %
inspect_atom(:key, _) (new)       27.77 K       36.01 μs    ±12.01%       35.70 μs       40.30 μs
inspect_atom(:key, _) (old)       11.01 K       90.79 μs    ±19.91%       88.22 μs      135.49 μs

Comparison:
inspect_atom(:key, _) (new)       27.77 K
inspect_atom(:key, _) (old)       11.01 K - 2.52x slower +54.78 μs

Memory usage statistics:

Name                           Memory usage
inspect_atom(:key, _) (new)        42.94 KB
inspect_atom(:key, _) (old)       399.78 KB - 9.31x memory usage +356.84 KB

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

##### With input 500 identifiers (short) #####
Name                                  ips        average  deviation         median         99th %
inspect_atom(:key, _) (new)       35.85 K       27.90 μs    ±14.04%       27.60 μs       32.29 μs
inspect_atom(:key, _) (old)       16.17 K       61.84 μs    ±21.10%       60.11 μs      100.18 μs

Comparison:
inspect_atom(:key, _) (new)       35.85 K
inspect_atom(:key, _) (old)       16.17 K - 2.22x slower +33.95 μs

Memory usage statistics:

Name                           Memory usage
inspect_atom(:key, _) (new)        39.04 KB
inspect_atom(:key, _) (old)       165.14 KB - 4.23x memory usage +126.10 KB

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

##### With input 500 identifiers with ? #####
Name                                  ips        average  deviation         median         99th %
inspect_atom(:key, _) (new)       30.14 K       33.18 μs    ±13.97%       32.82 μs       37.56 μs
inspect_atom(:key, _) (old)       12.91 K       77.44 μs    ±15.39%       76.26 μs       88.02 μs

Comparison:
inspect_atom(:key, _) (new)       30.14 K
inspect_atom(:key, _) (old)       12.91 K - 2.33x slower +44.26 μs

Memory usage statistics:

Name                           Memory usage
inspect_atom(:key, _) (new)        42.94 KB
inspect_atom(:key, _) (old)       314.38 KB - 7.32x memory usage +271.44 KB

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

##### With input 500 quoted atoms (slow path) #####
Name                                  ips        average  deviation         median         99th %
inspect_atom(:key, _) (old)        4.65 K      214.92 μs    ±11.11%      212.50 μs      265.73 μs
inspect_atom(:key, _) (new)        4.52 K      221.11 μs     ±8.53%      219.96 μs      233.50 μs

Comparison:
inspect_atom(:key, _) (old)        4.65 K
inspect_atom(:key, _) (new)        4.52 K - 1.03x slower +6.18 μs

Memory usage statistics:

Name                           Memory usage
inspect_atom(:key, _) (old)       306.08 KB
inspect_atom(:key, _) (new)       324.84 KB - 1.06x memory usage +18.76 KB

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

##### With input 500 unicode atoms (slow path) #####
Name                                  ips        average  deviation         median         99th %
inspect_atom(:key, _) (old)        5.67 K      176.37 μs    ±11.58%      174.73 μs      211.31 μs
inspect_atom(:key, _) (new)        5.52 K      181.06 μs    ±10.66%      179.50 μs      197.49 μs

Comparison:
inspect_atom(:key, _) (old)        5.67 K
inspect_atom(:key, _) (new)        5.52 K - 1.03x slower +4.69 μs

Memory usage statistics:

Name                           Memory usage
inspect_atom(:key, _) (old)       657.63 KB
inspect_atom(:key, _) (new)       668.48 KB - 1.02x memory usage +10.84 KB

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

##### With input 500 uppercase non-aliases (slow path) #####
Name                                  ips        average  deviation         median         99th %
inspect_atom(:key, _) (old)       10.35 K       96.64 μs    ±19.16%       94.00 μs      168.90 μs
inspect_atom(:key, _) (new)        9.78 K      102.23 μs    ±12.61%      101.06 μs      119.90 μs

Comparison:
inspect_atom(:key, _) (old)       10.35 K
inspect_atom(:key, _) (new)        9.78 K - 1.06x slower +5.59 μs

Memory usage statistics:

Name                           Memory usage
inspect_atom(:key, _) (old)       520.28 KB
inspect_atom(:key, _) (new)       540.47 KB - 1.04x memory usage +20.19 KB

Assisted-by: Claude Opus 5

dkuku and others added 3 commits August 18, 2026 22:59
Atoms made exclusively of ASCII letters, numbers and underscores, beginning
with a lowercase letter or underscore and optionally ending with ? or !, are
always identifiers, and only atoms prefixed by "Elixir" are candidates for
aliases. Recognizing both directly on the binary avoids building a charlist
and running the unicode aware identifier tokenizer, which dominates the cost
of classifying keyword list, map and struct keys when inspecting them.

Both are matched at once because the first byte tells them apart and matching
them separately would set up and throw away a second match context. For the
same reason the alias scan branches between two states instead of returning
the rest of the binary from a helper, which would build a sub binary per
piece.

The special form and quoted operator atoms move into guards so that no binary
is built for atoms that are not callable.

Inspecting a keyword list of 8 pairs goes from 10.4us to 8.9us (1.17x), a list
of 8 module atoms from 4.0us to 3.5us (1.15x). Atoms that still fall through
to the tokenizer pay ~5-10% for the extra scan.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The classification of ASCII identifiers and aliases is boundary heavy: only
one trailing ? or !, only at the end, and the "Elixir" prefix is an alias only
when every following piece starts with an uppercase letter. None of it was
covered, so add the cases that separate identifiers and aliases from atoms that
merely look like them.

These pass before and after the binary classification fast path, pinning it to
the tokenizer's verdict.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dkuku dkuku changed the title Dk classify atom binary fast path Macro.classify_atom - binary fast path Aug 19, 2026
Comment thread lib/elixir/lib/macro.ex
cond do
atom in [:%, :%{}, :{}, :<<>>, :..., :.., :., :..//, :->] ->
:not_callable
defp inner_classify(atom)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please keep it as a cond, as that change is unrelated to this pull request? Change only the code that affects the benchmark.

end
end

describe "classify_atom/1" do

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those test cases seem excessive and I believe we already test this functionality by proxy through other places.

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