Skip to content

Optimize unpercent with guard and by avoiding with clause - #15476

Merged
josevalim merged 1 commit into
elixir-lang:mainfrom
preciz:optimization1
Jun 14, 2026
Merged

Optimize unpercent with guard and by avoiding with clause#15476
josevalim merged 1 commit into
elixir-lang:mainfrom
preciz:optimization1

Conversation

@preciz

@preciz preciz commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Assisted by: Antigravity CLI : Gemini Flash 3.5

Instead of the with statement the optimized code uses a guard and pattern matching.

Bench:

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

defmodule OldURI do
  import Bitwise

  def decode_www_form(string) when is_binary(string) do
    unpercent(string, "", true)
  end

  defp unpercent(<<?+, tail::binary>>, acc, spaces = true) do
    unpercent(tail, <<acc::binary, ?\s>>, spaces)
  end

  defp unpercent(<<?%, tail::binary>>, acc, spaces) do
    with <<hex1, hex2, tail::binary>> <- tail,
         dec1 when is_integer(dec1) <- hex_to_dec(hex1),
         dec2 when is_integer(dec2) <- hex_to_dec(hex2) do
      unpercent(tail, <<acc::binary, bsl(dec1, 4) + dec2>>, spaces)
    else
      _ -> unpercent(tail, <<acc::binary, ?%>>, spaces)
    end
  end

  defp unpercent(<<head, tail::binary>>, acc, spaces) do
    unpercent(tail, <<acc::binary, head>>, spaces)
  end

  defp unpercent(<<>>, acc, _spaces), do: acc

  defp hex_to_dec(n) when n in ?A..?F, do: n - ?A + 10
  defp hex_to_dec(n) when n in ?a..?f, do: n - ?a + 10
  defp hex_to_dec(n) when n in ?0..?9, do: n - ?0
  defp hex_to_dec(_n), do: nil
end

defmodule NewURI do
  import Bitwise

  def decode_www_form(string) when is_binary(string) do
    unpercent(string, "", true)
  end

  defguardp is_hex(char) when char in ?0..?9 or char in ?A..?F or char in ?a..?f

  defp unpercent(<<?+, tail::binary>>, acc, spaces = true) do
    unpercent(tail, <<acc::binary, ?\s>>, spaces)
  end

  defp unpercent(<<?%, h1, h2, tail::binary>>, acc, spaces)
       when is_hex(h1) and is_hex(h2) do
    unpercent(tail, <<acc::binary, bsl(hex_to_dec(h1), 4) + hex_to_dec(h2)>>, spaces)
  end

  defp unpercent(<<?%, tail::binary>>, acc, spaces) do
    unpercent(tail, <<acc::binary, ?%>>, spaces)
  end

  defp unpercent(<<head, tail::binary>>, acc, spaces) do
    unpercent(tail, <<acc::binary, head>>, spaces)
  end

  defp unpercent(<<>>, acc, _spaces), do: acc

  defp hex_to_dec(n) when n in ?A..?F, do: n - ?A + 10
  defp hex_to_dec(n) when n in ?a..?f, do: n - ?a + 10
  defp hex_to_dec(n) when n in ?0..?9, do: n - ?0
end

inputs = %{
  "Plain String (No percent-encoding)" => "Hello, World! This is a simple test string with no special characters.",
  "Fully Percent-Encoded" => "%48%65%6c%6c%6f%2c%20%57%6f%72%6c%64%21%20%54%68%69%73%20%69%73%20%61%20%73%69%6d%70%6c%65%20%74%65%73%74%20%73%74%72%69%6e%67%20%77%69%74%68%20%6e%6f%20%73%70%65%63%69%61%6c%20%63%68%61%72%61%63%74%65%72%73%2e",
  "Mixed (Partially percent-encoded, + signs)" => "Hello+World%21+This%20is%20a%20test%20with%20some%20%2b%20characters%20and%20%25%20symbols.",
  "Malformed / Invalid Percent-Encoded" => "Hello%2gWorld%2%z+foo%bar%1"
}

Benchee.run(
  %{
    "Old Version (with/else)" => fn input -> OldURI.decode_www_form(input) end,
    "New Version (guards/multiple clauses)" => fn input -> NewURI.decode_www_form(input) end
  },
  inputs: inputs,
  time: 2,
  memory_time: 1
)

Results on noisy 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: 1 s
reduction time: 0 ns
parallel: 1
inputs: Fully Percent-Encoded, Malformed / Invalid Percent-Encoded, Mixed (Partially percent-encoded, + signs), Plain String (No percent-encoding)
Estimated total run time: 40 s
Excluding outliers: false

##### With input Fully Percent-Encoded #####
Name                                            ips        average  deviation         median         99th %
New Version (guards/multiple clauses)      992.45 K        1.01 μs   ±112.36%        0.97 μs        1.30 μs
Old Version (with/else)                    720.59 K        1.39 μs   ±520.75%        1.23 μs        2.50 μs

Comparison:
New Version (guards/multiple clauses)      992.45 K
Old Version (with/else)                    720.59 K - 1.38x slower +0.38 μs

Memory usage statistics:

Name                                     Memory usage
New Version (guards/multiple clauses)        0.102 KB
Old Version (with/else)                       3.07 KB - 30.23x memory usage +2.97 KB

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

##### With input Malformed / Invalid Percent-Encoded #####
Name                                            ips        average  deviation         median         99th %
New Version (guards/multiple clauses)        4.15 M      240.88 ns   ±292.22%         230 ns         370 ns
Old Version (with/else)                      3.50 M      285.66 ns   ±562.92%         261 ns         441 ns

Comparison:
New Version (guards/multiple clauses)        4.15 M
Old Version (with/else)                      3.50 M - 1.19x slower +44.79 ns

Memory usage statistics:

Name                                     Memory usage
New Version (guards/multiple clauses)           104 B
Old Version (with/else)                         416 B - 4.00x memory usage +312 B

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

##### With input Mixed (Partially percent-encoded, + signs) #####
Name                                            ips        average  deviation         median         99th %
New Version (guards/multiple clauses)        1.75 M      572.21 ns   ±177.36%         551 ns         791 ns
Old Version (with/else)                      1.52 M      656.50 ns   ±265.55%         611 ns        1112 ns

Comparison:
New Version (guards/multiple clauses)        1.75 M
Old Version (with/else)                      1.52 M - 1.15x slower +84.29 ns

Memory usage statistics:

Name                                     Memory usage
New Version (guards/multiple clauses)           104 B
Old Version (with/else)                         752 B - 7.23x memory usage +648 B

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

##### With input Plain String (No percent-encoding) #####
Name                                            ips        average  deviation         median         99th %
Old Version (with/else)                      2.01 M      497.81 ns   ±218.92%         481 ns         691 ns
New Version (guards/multiple clauses)        1.97 M      506.94 ns   ±246.30%         491 ns         671 ns

Comparison:
Old Version (with/else)                      2.01 M
New Version (guards/multiple clauses)        1.97 M - 1.02x slower +9.13 ns

Memory usage statistics:

Name                                     Memory usage
Old Version (with/else)                         104 B
New Version (guards/multiple clauses)           104 B - 1.00x memory usage +0 B

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

@josevalim
josevalim merged commit 1796733 into elixir-lang:main Jun 14, 2026
12 of 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