Skip to content

Optimize Integer.parse/2 by matching digit ranges - #16037

Merged
josevalim merged 1 commit into
elixir-lang:mainfrom
preciz:perf/integer-parse
Oct 30, 2026
Merged

josevalim merged 1 commit into
elixir-lang:mainfrom
preciz:perf/integer-parse

Conversation

@preciz

@preciz preciz commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Assisted-by: Antigravity:Gemini-4-Argon

Faster or same speed.
Lower or same memory usage.
.beam size same (slightly smaller)

Replacing the 62 unrolled character clauses with range guards and a
base-10 fast path eliminates a 75-entry indirect jump table across 36
branch targets, allowing the JIT to fuse bounds checks into is_in_range
and use tagless, overflow-free arithmetic with the 2..36 base type
bounds. Matching the first digit directly in count_digits/2 also avoids
bitstring position backtracking on unsigned inputs, while the
count == byte_size(binary) fast path skips :erlang.split_binary/2 and
its tuple/sub-binary allocation when the entire binary is parsed.

Bench:

Mix.install([:benchee])

path = "lib/elixir/lib/integer.ex"
baseline_ref = "c2f16efdc"

{baseline_source, 0} = System.cmd("git", ["show", "#{baseline_ref}:#{path}"], cd: __DIR__)

for {source, module} <- [{baseline_source, Old}, {File.read!(Path.join(__DIR__, path)), New}] do
  source
  |> String.replace("defmodule Integer do", "defmodule #{inspect(module)} do")
  |> Code.compile_string(path)
end

base10_short = "12345"
base10_signed = "-9876543210"
base10_medium = String.duplicate("1234567890", 2)
base10_long = String.duplicate("1234567890", 50)
base10_with_rem = "404 not found"
base10_long_with_rem = base10_long <> " not found"
base16_short_lower = "deadbeef"
base16_short_lower_with_rem = "deadbeef_rem"
base16_medium = "deadBEEF0123456789abcdef"
base16_long = String.duplicate("deadBEEF0123456789abcdef", 20)
base16_long_with_rem = base16_long <> "_rem"
base36_medium = "ElixirIsAwesome1234567890"

base36_long =
  String.duplicate("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", 10)

base2_long = String.duplicate("10110011", 64)
invalid = "not_an_integer"
invalid_sign_only = "-not_an_integer"

inputs = %{
  "base 10 (short, 5 digits)" => {base10_short, 10},
  "base 10 (signed, 10 digits)" => {base10_signed, 10},
  "base 10 (medium, 20 digits)" => {base10_medium, 10},
  "base 10 (long, 500 digits)" => {base10_long, 10},
  "base 10 (with remainder)" => {base10_with_rem, 10},
  "base 10 (long, with remainder)" => {base10_long_with_rem, 10},
  "base 2 (long, 512 digits)" => {base2_long, 2},
  "base 16 (short lowercase, 8 digits)" => {base16_short_lower, 16},
  "base 16 (short lowercase, with remainder)" => {base16_short_lower_with_rem, 16},
  "base 16 (medium, 24 digits)" => {base16_medium, 16},
  "base 16 (long, 480 digits)" => {base16_long, 16},
  "base 16 (long, with remainder)" => {base16_long_with_rem, 16},
  "base 36 (medium, 25 digits)" => {base36_medium, 36},
  "base 36 (long, 620 digits)" => {base36_long, 36},
  "invalid (empty)" => {"", 10},
  "invalid (no digits)" => {invalid, 10},
  "invalid (base 16, no digits)" => {invalid, 16},
  "invalid (sign only)" => {invalid_sign_only, 10}
}

Benchee.run(
  %{
    "a#{baseline_ref} (baseline)" => fn {bin, base} -> Old.parse(bin, base) end,
    "working tree (optimized)" => fn {bin, base} -> New.parse(bin, base) end
  },
  inputs: inputs,
  pre_check: :all_same,
  warmup: 1,
  time: 3,
  memory_time: 1
)

Results:

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

Benchmark suite executing with the following configuration:
warmup: 1 s
time: 3 s
memory time: 1 s
reduction time: 0 ns
parallel: 1
inputs: base 10 (long, 500 digits), base 10 (long, with remainder), base 10 (medium, 20 digits), base 10 (short, 5 digits), base 10 (signed, 10 digits), base 10 (with remainder), base 16 (long, 480 digits), base 16 (long, with remainder), base 16 (medium, 24 digits), base 16 (short lowercase, 8 digits), base 16 (short lowercase, with remainder), base 2 (long, 512 digits), base 36 (long, 620 digits), base 36 (medium, 25 digits), invalid (base 16, no digits), invalid (empty), invalid (no digits), invalid (sign only)
Estimated total run time: 3 min
Excluding outliers: false

##### With input base 10 (long, 500 digits) #####
Name                               ips        average  deviation         median         99th %
working tree (optimized)      395.60 K        2.53 μs   ±188.43%        2.42 μs        3.92 μs
ac2f16efdc (baseline)         324.01 K        3.09 μs   ±170.28%        2.85 μs        5.47 μs

Comparison: 
working tree (optimized)      395.60 K
ac2f16efdc (baseline)         324.01 K - 1.22x slower +0.56 μs

Memory usage statistics:

Name                        Memory usage
working tree (optimized)         2.36 KB
ac2f16efdc (baseline)            2.44 KB - 1.03x memory usage +0.0781 KB

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

##### With input base 10 (long, with remainder) #####
Name                               ips        average  deviation         median         99th %
working tree (optimized)      387.48 K        2.58 μs   ±262.06%        2.47 μs        3.89 μs
ac2f16efdc (baseline)         180.04 K        5.55 μs   ±116.65%        5.39 μs        8.00 μs

Comparison: 
working tree (optimized)      387.48 K
ac2f16efdc (baseline)         180.04 K - 2.15x slower +2.97 μs

Memory usage statistics:

Name                        Memory usage
working tree (optimized)         2.45 KB
ac2f16efdc (baseline)            2.45 KB - 1.00x memory usage +0 KB

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

##### With input base 10 (medium, 20 digits) #####
Name                               ips        average  deviation         median         99th %
working tree (optimized)        5.20 M      192.36 ns  ±2251.27%         170 ns         330 ns
ac2f16efdc (baseline)           3.85 M      259.49 ns  ±1675.17%         221 ns         381 ns

Comparison: 
working tree (optimized)        5.20 M
ac2f16efdc (baseline)           3.85 M - 1.35x slower +67.13 ns

Memory usage statistics:

Name                        Memory usage
working tree (optimized)           400 B
ac2f16efdc (baseline)              480 B - 1.20x memory usage +80 B

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

##### With input base 10 (short, 5 digits) #####
Name                               ips        average  deviation         median         99th %
working tree (optimized)       17.21 M       58.10 ns  ±8591.75%          41 ns          70 ns
ac2f16efdc (baseline)          13.59 M       73.57 ns  ±1168.22%          70 ns         101 ns

Comparison: 
working tree (optimized)       17.21 M
ac2f16efdc (baseline)          13.59 M - 1.27x slower +15.47 ns

Memory usage statistics:

Name                        Memory usage
working tree (optimized)            64 B
ac2f16efdc (baseline)              128 B - 2.00x memory usage +64 B

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

##### With input base 10 (signed, 10 digits) #####
Name                               ips        average  deviation         median         99th %
working tree (optimized)       13.30 M       75.21 ns  ±8769.63%          60 ns          90 ns
ac2f16efdc (baseline)          10.84 M       92.25 ns  ±3179.26%          80 ns         130 ns

Comparison: 
working tree (optimized)       13.30 M
ac2f16efdc (baseline)          10.84 M - 1.23x slower +17.04 ns

Memory usage statistics:

Name                        Memory usage
working tree (optimized)            64 B
ac2f16efdc (baseline)              136 B - 2.13x memory usage +72 B

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

##### With input base 10 (with remainder) #####
Name                               ips        average  deviation         median         99th %
working tree (optimized)       13.01 M       76.85 ns  ±4666.94%          60 ns         100 ns
ac2f16efdc (baseline)          12.32 M       81.16 ns  ±4661.15%          70 ns         100 ns

Comparison: 
working tree (optimized)       13.01 M
ac2f16efdc (baseline)          12.32 M - 1.06x slower +4.31 ns

Memory usage statistics:

Name                        Memory usage
working tree (optimized)           144 B
ac2f16efdc (baseline)              144 B - 1.00x memory usage +0 B

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

##### With input base 16 (long, 480 digits) #####
Name                               ips        average  deviation         median         99th %
working tree (optimized)      320.55 K        3.12 μs   ±227.13%        2.98 μs        4.73 μs
ac2f16efdc (baseline)         208.64 K        4.79 μs   ±155.76%        5.51 μs        7.50 μs

Comparison: 
working tree (optimized)      320.55 K
ac2f16efdc (baseline)         208.64 K - 1.54x slower +1.67 μs

Memory usage statistics:

Name                        Memory usage
working tree (optimized)         2.45 KB
ac2f16efdc (baseline)            2.52 KB - 1.03x memory usage +0.0781 KB

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

##### With input base 16 (long, with remainder) #####
Name                               ips        average  deviation         median         99th %
working tree (optimized)      313.99 K        3.18 μs   ±237.66%        3.06 μs        4.80 μs
ac2f16efdc (baseline)         237.07 K        4.22 μs   ±120.80%        3.44 μs        7.25 μs

Comparison: 
working tree (optimized)      313.99 K
ac2f16efdc (baseline)         237.07 K - 1.32x slower +1.03 μs

Memory usage statistics:

Name                        Memory usage
working tree (optimized)         2.53 KB
ac2f16efdc (baseline)            2.53 KB - 1.00x memory usage +0 KB

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

##### With input base 16 (medium, 24 digits) #####
Name                               ips        average  deviation         median         99th %
working tree (optimized)        4.36 M      229.25 ns  ±1658.61%         210 ns         371 ns
ac2f16efdc (baseline)           2.85 M      351.34 ns  ±1698.47%         311 ns         551 ns

Comparison: 
working tree (optimized)        4.36 M
ac2f16efdc (baseline)           2.85 M - 1.53x slower +122.09 ns

Memory usage statistics:

Name                        Memory usage
working tree (optimized)           400 B
ac2f16efdc (baseline)              480 B - 1.20x memory usage +80 B

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

##### With input base 16 (short lowercase, 8 digits) #####
Name                               ips        average  deviation         median         99th %
working tree (optimized)       13.69 M       73.06 ns  ±8229.39%          60 ns         100 ns
ac2f16efdc (baseline)          11.72 M       85.31 ns  ±1519.53%          80 ns         121 ns

Comparison: 
working tree (optimized)       13.69 M
ac2f16efdc (baseline)          11.72 M - 1.17x slower +12.25 ns

Memory usage statistics:

Name                        Memory usage
working tree (optimized)            64 B
ac2f16efdc (baseline)              128 B - 2.00x memory usage +64 B

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

##### With input base 16 (short lowercase, with remainder) #####
Name                               ips        average  deviation         median         99th %
working tree (optimized)       11.17 M       89.50 ns  ±2707.31%          80 ns         120 ns
ac2f16efdc (baseline)          10.85 M       92.12 ns  ±3120.87%          80 ns         120 ns

Comparison: 
working tree (optimized)       11.17 M
ac2f16efdc (baseline)          10.85 M - 1.03x slower +2.62 ns

Memory usage statistics:

Name                        Memory usage
working tree (optimized)           136 B
ac2f16efdc (baseline)              136 B - 1.00x memory usage +0 B

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

##### With input base 2 (long, 512 digits) #####
Name                               ips        average  deviation         median         99th %
working tree (optimized)      521.40 K        1.92 μs   ±345.64%        1.83 μs        2.59 μs
ac2f16efdc (baseline)         306.43 K        3.26 μs   ±175.08%        3.37 μs        4.60 μs

Comparison: 
working tree (optimized)      521.40 K
ac2f16efdc (baseline)         306.43 K - 1.70x slower +1.35 μs

Memory usage statistics:

Name                        Memory usage
working tree (optimized)         1.23 KB
ac2f16efdc (baseline)            1.30 KB - 1.06x memory usage +0.0781 KB

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

##### With input base 36 (long, 620 digits) #####
Name                               ips        average  deviation         median         99th %
working tree (optimized)      183.52 K        5.45 μs   ±108.83%        5.24 μs        8.19 μs
ac2f16efdc (baseline)         151.36 K        6.61 μs    ±44.12%        6.98 μs        9.56 μs

Comparison: 
working tree (optimized)      183.52 K
ac2f16efdc (baseline)         151.36 K - 1.21x slower +1.16 μs

Memory usage statistics:

Name                        Memory usage
working tree (optimized)         3.82 KB
ac2f16efdc (baseline)            3.90 KB - 1.02x memory usage +0.0781 KB

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

##### With input base 36 (medium, 25 digits) #####
Name                               ips        average  deviation         median         99th %
working tree (optimized)        3.46 M      288.73 ns  ±1622.65%         260 ns         441 ns
ac2f16efdc (baseline)           2.79 M      358.41 ns    ±80.91%         360 ns         491 ns

Comparison: 
working tree (optimized)        3.46 M
ac2f16efdc (baseline)           2.79 M - 1.24x slower +69.68 ns

Memory usage statistics:

Name                        Memory usage
working tree (optimized)           472 B
ac2f16efdc (baseline)              560 B - 1.19x memory usage +88 B

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

##### With input invalid (base 16, no digits) #####
Name                               ips        average  deviation         median         99th %
working tree (optimized)       28.72 M       34.82 ns  ±1562.68%          30 ns          50 ns
ac2f16efdc (baseline)          26.88 M       37.21 ns  ±1521.73%          30 ns          50 ns

Comparison: 
working tree (optimized)       28.72 M
ac2f16efdc (baseline)          26.88 M - 1.07x slower +2.38 ns

Memory usage statistics:

Name                        Memory usage
working tree (optimized)            40 B
ac2f16efdc (baseline)               40 B - 1.00x memory usage +0 B

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

##### With input invalid (empty) #####
Name                               ips        average  deviation         median         99th %
working tree (optimized)       30.09 M       33.23 ns  ±1675.37%          30 ns          40 ns
ac2f16efdc (baseline)          29.25 M       34.19 ns  ±1849.49%          30 ns          50 ns

Comparison: 
working tree (optimized)       30.09 M
ac2f16efdc (baseline)          29.25 M - 1.03x slower +0.95 ns

Memory usage statistics:

Name                        Memory usage
working tree (optimized)            40 B
ac2f16efdc (baseline)               40 B - 1.00x memory usage +0 B

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

##### With input invalid (no digits) #####
Name                               ips        average  deviation         median         99th %
working tree (optimized)       28.99 M       34.49 ns  ±1964.59%          30 ns          50 ns
ac2f16efdc (baseline)          25.86 M       38.67 ns  ±1632.46%          40 ns          50 ns

Comparison: 
working tree (optimized)       28.99 M
ac2f16efdc (baseline)          25.86 M - 1.12x slower +4.18 ns

Memory usage statistics:

Name                        Memory usage
working tree (optimized)            40 B
ac2f16efdc (baseline)               40 B - 1.00x memory usage +0 B

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

##### With input invalid (sign only) #####
Name                               ips        average  deviation         median         99th %
working tree (optimized)       27.84 M       35.92 ns  ±1562.84%          30 ns          50 ns
ac2f16efdc (baseline)          26.04 M       38.40 ns  ±1495.80%          31 ns          50 ns

Comparison: 
working tree (optimized)       27.84 M
ac2f16efdc (baseline)          26.04 M - 1.07x slower +2.47 ns

Memory usage statistics:

Name                        Memory usage
working tree (optimized)            40 B
ac2f16efdc (baseline)               40 B - 1.00x memory usage +0 B

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

Replacing the 62 unrolled character clauses with range guards and a
base-10 fast path eliminates a 75-entry indirect jump table across 36
branch targets, allowing the JIT to fuse bounds checks into is_in_range
and use tagless, overflow-free arithmetic with the 2..36 base type
bounds. Matching the first digit directly in count_digits/2 also avoids
bitstring position backtracking on unsigned inputs, while the
count == byte_size(binary) fast path skips :erlang.split_binary/2 and
its tuple/sub-binary allocation when the entire binary is parsed.

Assisted-by: Antigravity:Gemini-4-Argon
@rdlu

rdlu commented Oct 29, 2026 •

Copy link
Copy Markdown

Assisted-by: Claude Code 2.1.311:claude-opus-5-5

Independent verification of this PR on a different machine (AMD Ryzen 7 7735HS, Elixir 1.20.4, OTP 29.1.1, JIT).

Correctness: fuzzed Old.parse/2 (c2f16ef) vs New.parse/2 (3fa127d) on 300,840 inputs — random strings over 0-9a-zA-Z+-_ ., control and high bytes, all bases 2..36, plus edge cases (empty, lone sign, +-1, the chars adjacent to each range like / : @ [ ` {, unicode digits). 0 mismatches. Error behaviour is identical too (ArgumentError for bases 1, 37, 10.0, :a; FunctionClauseError for non-binaries).

Benchmark: the script from the description plus a few extra inputs, run twice with longer settings (2s warmup / 5s time, then 3s warmup / 15s time). Speedup of the PR over the baseline:

Input PR description 5s run 15s run
base 10, 1 digit (extra) — 1.67x 1.63x
base 10, 5 digits 1.27x 1.68x 1.62x
base 10, signed 10 digits 1.23x 1.48x 1.44x
base 10, 20 digits 1.35x 1.27x 1.25x
base 16, 8 digits lowercase 1.17x 1.33x 1.36x
base 16, 8 digits uppercase (extra) — 1.31x 1.32x
base 2, 8 digits (extra) — 1.49x 1.42x
base 10, 500 digits 1.22x 2.32x 1.57x
base 10, 500 digits + remainder 2.15x 2.20x 1.61x
base 16, 480 digits 1.54x 1.60x 1.33x
base 2, 512 digits 1.70x 2.01x 2.39x
base 36, 620 digits 1.21x 1.68x 1.68x
short with remainder ("404 not found", "-42px", "deadbeef_rem") 1.03–1.06x 1.00–1.06x 1.00–1.12x
stops at first out-of-base char ("7777777781" base 8, "ffffffffg" base 16) (extra) — 1.00–1.03x 1.01x
invalid inputs 1.03–1.12x 1.05–1.07x 1.01–1.11x

Faster or equal on every input in both runs. Short all-digit inputs are consistently 1.25–1.65x; long inputs vary run to run (bignum allocation/GC) but were always ≥1.2x. Short inputs with a remainder are roughly even, as expected since the count == byte_size(binary) fast path doesn't apply. Memory numbers match the description exactly (e.g. 128 B → 64 B for short full-digit inputs). .beam size: 19,528 → 19,292 bytes.

Suggestion: the existing parse/2 test doesn't cover the range-boundary cases the new guards depend on. Something like this could go into integer_test.exs (passes on both baseline and this PR):

assert Integer.parse("78", 8) === {7, "8"}
assert Integer.parse("fg", 16) === {15, "g"}
assert Integer.parse("FF", 16) === {255, ""}
assert Integer.parse("zZ", 36) === {1295, ""}
assert Integer.parse("9", 9) === :error
assert Integer.parse("-g", 16) === :error
assert Integer.parse("Zz", 35) === :error

Minor: the base in 2..36 added to the clause guard is redundant at runtime (the clause above already raises), so a short comment noting it's there for the compiler/JIT might help future readers.

@josevalim
josevalim merged commit 669f848 into elixir-lang:main Oct 30, 2026
15 checks passed
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