Skip to content

remove intermediate list in float parse - #15719

Merged
josevalim merged 2 commits into
elixir-lang:mainfrom
dkuku:dk/avoid_intermediate_list_in_float_parse
Aug 7, 2026
Merged

remove intermediate list in float parse#15719
josevalim merged 2 commits into
elixir-lang:mainfrom
dkuku:dk/avoid_intermediate_list_in_float_parse

Conversation

@dkuku

@dkuku dkuku commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

This change validates the input to Float.parse on the original binary and passes a slice of the original binary to :erlang.binary_to_float() or :erlang.binary_to_interger()

Benchmarked it for different inputs and it always uses less memory and is almost always faster. The only slower edge case is a float without a dot 1e3 where it requires an additional .0 to be added to be correctly parsed - first 2 examples in the benchmark - I think this is an edge case - the correct version should be 1.0e3.
Integer input is 2x faster - it just calculates integer * 1.0.

Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com

@dkuku

dkuku commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

benchmark;

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

defmodule FloatOld do
  @moduledoc "Previous implementation: builds a reversed char list."

  def parse("-" <> binary) do
    case parse_unsigned(binary) do
      :error -> :error
      {number, remainder} -> {-number, remainder}
    end
  end

  def parse("+" <> binary), do: parse_unsigned(binary)
  def parse(binary), do: parse_unsigned(binary)

  defp parse_unsigned(<<digit, rest::binary>>) when digit in ?0..?9,
    do: parse_unsigned(rest, false, false, [digit])

  defp parse_unsigned(binary) when is_binary(binary), do: :error

  defp parse_unsigned(<<digit, rest::binary>>, dot?, e?, acc) when digit in ?0..?9,
    do: parse_unsigned(rest, dot?, e?, [digit | acc])

  defp parse_unsigned(<<?., digit, rest::binary>>, false, false, acc) when digit in ?0..?9,
    do: parse_unsigned(rest, true, false, [digit, ?. | acc])

  defp parse_unsigned(<<exp_marker, digit, rest::binary>>, dot?, false, acc)
       when exp_marker in ~c"eE" and digit in ?0..?9,
       do: parse_unsigned(rest, true, true, [digit, ?e | add_dot(acc, dot?)])

  defp parse_unsigned(<<exp_marker, sign, digit, rest::binary>>, dot?, false, acc)
       when exp_marker in ~c"eE" and sign in ~c"-+" and digit in ?0..?9,
       do: parse_unsigned(rest, true, true, [digit, sign, ?e | add_dot(acc, dot?)])

  defp parse_unsigned(rest, dot?, _, acc) do
    acc
    |> add_dot(dot?)
    |> :lists.reverse()
    |> :erlang.list_to_float()
  rescue
    ArgumentError -> :error
  else
    float -> {float, rest}
  end

  defp add_dot(acc, true), do: acc
  defp add_dot(acc, false), do: [?0, ?. | acc]
end

inputs = %{
  "int-exponent \"34e5\"" => "34e5",
  "int-exponent signed \"34e-5\"" => "34e-5",
  "int-exponent trailing \"34e5xyz\"" => "34e5xyz",
  "integer \"34\"" => "34",
  "integer \"1234567\"" => "1234567",
  "decimal \"34.25\"" => "34.25",
  "decimal \"3.14159265358979\"" => "3.14159265358979",
  "scientific \"1.5e10\"" => "1.5e10",
  "scientific \"6.022e23\"" => "6.022e23",
  "negative \"-273.15\"" => "-273.15",
  "trailing \"56.5xyz\"" => "56.5xyz",
  "long decimal" => String.duplicate("1", 40) <> "." <> String.duplicate("2", 40)
}

# Both implementations must agree before benchmarking.
for {label, input} <- inputs do
  results = [Float.parse(input), FloatOld.parse(input)]

  unless match?([r, r], results) do
    raise "mismatch for #{label}: Float=#{inspect(Enum.at(results, 0))} " <>
            "FloatOld=#{inspect(Enum.at(results, 1))}"
  end
end

IO.puts("Both implementations agree. Running Benchee...\n")

Benchee.run(
  %{
    "Float (current)" => fn input -> Float.parse(input) end,
    "FloatOld" => fn input -> FloatOld.parse(input) end
  },
  inputs: inputs,
  warmup: 1,
  time: 1,
  memory_time: 1
)

@dkuku

dkuku commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

results:


##### With input int-exponent "34e5" #####
Name                      ips        average  deviation         median         99th %
FloatOld               6.68 M      149.60 ns  ±4943.32%         120 ns         221 ns
Float (current)        3.61 M      277.35 ns  ±1651.06%         210 ns        1413 ns

Comparison:
FloatOld               6.68 M
Float (current)        3.61 M - 1.85x slower +127.76 ns

Memory usage statistics:

Name               Memory usage
FloatOld                  288 B
Float (current)           184 B - 0.64x memory usage -104 B


##### With input int-exponent signed "34e-5" #####
Name                      ips        average  deviation         median         99th %
FloatOld               5.72 M      174.86 ns  ±4612.21%         140 ns         201 ns
Float (current)        3.51 M      284.70 ns  ±1567.16%         231 ns        1383 ns

Comparison:
FloatOld               5.72 M
Float (current)        3.51 M - 1.63x slower +109.84 ns

Memory usage statistics:

Name               Memory usage
FloatOld                  320 B
Float (current)           184 B - 0.57x memory usage -136 B


##### With input int-exponent trailing "34e5xyz" #####
Name                      ips        average  deviation         median         99th %
FloatOld               6.53 M      153.04 ns  ±4098.75%         120 ns         200 ns
Float (current)        3.56 M      281.14 ns  ±1911.86%         210 ns        1363 ns

Comparison:
FloatOld               6.53 M
Float (current)        3.56 M - 1.84x slower +128.10 ns

Memory usage statistics:

Name               Memory usage
FloatOld                  296 B
Float (current)           192 B - 0.65x memory usage -104 B


##### With input decimal "3.14159265358979" #####
Name                      ips        average  deviation         median         99th %
Float (current)        5.56 M      179.88 ns  ±3804.16%         160 ns         300 ns
FloatOld               4.33 M      230.79 ns  ±2516.39%         191 ns         361 ns

Comparison:
Float (current)        5.56 M
FloatOld               4.33 M - 1.28x slower +50.91 ns

Memory usage statistics:

Name               Memory usage
Float (current)           128 B
FloatOld                  608 B - 4.75x memory usage +480 B


##### With input decimal "34.25" #####
Name                      ips        average  deviation         median         99th %
Float (current)        6.78 M      147.58 ns  ±5074.43%         121 ns         190 ns
FloatOld               6.25 M      159.98 ns  ±4808.09%         130 ns         251 ns

Comparison:
Float (current)        6.78 M
FloatOld               6.25 M - 1.08x slower +12.40 ns

Memory usage statistics:

Name               Memory usage
Float (current)           120 B
FloatOld                  256 B - 2.13x memory usage +136 B


##### With input integer "1234567" #####
Name                      ips        average  deviation         median         99th %
Float (current)       12.15 M       82.33 ns  ±5891.43%          60 ns         110 ns
FloatOld               6.06 M      165.04 ns  ±3652.72%         130 ns         290 ns

Comparison:
Float (current)       12.15 M
FloatOld               6.06 M - 2.00x slower +82.71 ns

Memory usage statistics:

Name               Memory usage
Float (current)           120 B
FloatOld                  384 B - 3.20x memory usage +264 B


##### With input integer "34" #####
Name                      ips        average  deviation         median         99th %
Float (current)       14.48 M       69.07 ns  ±7223.35%          50 ns          80 ns
FloatOld               7.39 M      135.26 ns  ±4570.69%         100 ns         191 ns

Comparison:
Float (current)       14.48 M
FloatOld               7.39 M - 1.96x slower +66.18 ns

Memory usage statistics:

Name               Memory usage
Float (current)           120 B
FloatOld                  224 B - 1.87x memory usage +104 B


##### With input long decimal #####
Name                      ips        average  deviation         median         99th %
Float (current)        3.01 M      331.83 ns  ±1637.08%         310 ns         471 ns
FloatOld               1.66 M      602.59 ns  ±1185.38%         541 ns         822 ns

Comparison:
Float (current)        3.01 M
FloatOld               1.66 M - 1.82x slower +270.76 ns

Memory usage statistics:

Name               Memory usage
Float (current)        0.133 KB
FloatOld                1.58 KB - 11.88x memory usage +1.45 KB


##### With input negative "-273.15" #####
Name                      ips        average  deviation         median         99th %
FloatOld               6.16 M      162.45 ns   ±523.80%         150 ns         281 ns
Float (current)        5.75 M      174.02 ns  ±3475.28%         140 ns         261 ns

Comparison:
FloatOld               6.16 M
Float (current)        5.75 M - 1.07x slower +11.57 ns

Memory usage statistics:

Name               Memory usage
FloatOld                  312 B
Float (current)           144 B - 0.46x memory usage -168 B


##### With input scientific "1.5e10" #####
Name                      ips        average  deviation         median         99th %
Float (current)        6.66 M      150.22 ns  ±3420.57%         130 ns         211 ns
FloatOld               6.55 M      152.70 ns  ±5004.33%         120 ns         181 ns

Comparison:
Float (current)        6.66 M
FloatOld               6.55 M - 1.02x slower +2.47 ns

Memory usage statistics:

Name               Memory usage
Float (current)           120 B
FloatOld                  288 B - 2.40x memory usage +168 B


##### With input scientific "6.022e23" #####
Name                      ips        average  deviation         median         99th %
Float (current)        6.32 M      158.30 ns  ±3542.89%         131 ns         220 ns
FloatOld               5.74 M      174.13 ns  ±2492.93%         150 ns         270 ns

Comparison:
Float (current)        6.32 M
FloatOld               5.74 M - 1.10x slower +15.83 ns

Memory usage statistics:

Name               Memory usage
Float (current)           120 B
FloatOld                  352 B - 2.93x memory usage +232 B


##### With input trailing "56.5xyz" #####
Name                      ips        average  deviation         median         99th %
FloatOld               6.41 M      156.02 ns  ±3831.06%         121 ns         201 ns
Float (current)        6.28 M      159.25 ns  ±1860.85%         150 ns         280 ns

Comparison:
FloatOld               6.41 M
Float (current)        6.28 M - 1.02x slower +3.23 ns

Memory usage statistics:

Name               Memory usage
FloatOld                  232 B
Float (current)           128 B - 0.55x memory usage -104 B

@dkuku

dkuku commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

with iodata_to_binary it is faster on the integer with exponent path:

##### With input int-exponent "34e5" #####
Name                      ips        average  deviation         median         99th %
FloatOld               6.53 M      153.04 ns  ±5814.86%         120 ns         181 ns
Float (current)        4.41 M      226.58 ns  ±3370.24%         181 ns         340 ns

Comparison:
FloatOld               6.53 M
Float (current)        4.41 M - 1.48x slower +73.53 ns

Memory usage statistics:

Name               Memory usage
FloatOld                  288 B
Float (current)           192 B - 0.67x memory usage -96 B

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

##### With input int-exponent signed "34e-5" #####
Name                      ips        average  deviation         median         99th %
FloatOld               5.74 M      174.30 ns  ±4524.43%         140 ns         250 ns
Float (current)        3.96 M      252.33 ns  ±2753.32%         210 ns         361 ns

Comparison:
FloatOld               5.74 M
Float (current)        3.96 M - 1.45x slower +78.03 ns

Memory usage statistics:

Name               Memory usage
FloatOld                  320 B
Float (current)           192 B - 0.60x memory usage -128 B

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

##### With input int-exponent trailing "34e5xyz" #####
Name                      ips        average  deviation         median         99th %
FloatOld               6.48 M      154.30 ns  ±4189.96%         121 ns         201 ns
Float (current)        4.46 M      224.11 ns  ±2546.46%         190 ns         341 ns

Comparison:
FloatOld               6.48 M
Float (current)        4.46 M - 1.45x slower +69.82 ns

Memory usage statistics:

Name               Memory usage
FloatOld                  296 B
Float (current)           200 B - 0.68x memory usage -96 B

@josevalim josevalim closed this Aug 6, 2026
@josevalim josevalim reopened this Aug 6, 2026
@josevalim josevalim closed this Aug 7, 2026
@josevalim josevalim reopened this Aug 7, 2026
@josevalim
josevalim merged commit e697189 into elixir-lang:main Aug 7, 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