Skip to content

Optimize URI.to_string/1 - #15477

Merged
josevalim merged 2 commits into
elixir-lang:mainfrom
preciz:optimization6
Jun 14, 2026
Merged

Optimize URI.to_string/1#15477
josevalim merged 2 commits into
elixir-lang:mainfrom
preciz:optimization6

Conversation

@preciz

@preciz preciz commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Assisted by: Antigravity CLI & Gemini 3.5 Flash (hand rolled in the end, don't blame the LLM 😄 )

What this does:

  • Avoids the map update.
  • Destructures the URI once.
  • Defers a condition to extract_authority.

It is a small performance win in most cases and it uses less memory in almost all cases.

Bench:

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

defmodule OldURI do
  def to_string(%{scheme: scheme, port: port, path: path, query: query, fragment: fragment} = uri) do
    uri =
      case scheme && URI.default_port(scheme) do
        ^port -> %{uri | port: nil}
        _ -> uri
      end

    authority = extract_authority(uri)

    IO.iodata_to_binary([
      if(scheme, do: [scheme, ?:], else: []),
      if(authority, do: ["//" | authority], else: []),
      if(path, do: path, else: []),
      if(query, do: ["?" | query], else: []),
      if(fragment, do: ["#" | fragment], else: [])
    ])
  end

  defp extract_authority(%{host: nil, authority: authority}) do
    authority
  end

  defp extract_authority(%{host: host, userinfo: userinfo, port: port}) do
    [
      if(userinfo, do: [userinfo | "@"], else: []),
      if(String.contains?(host, ":"), do: ["[", host | "]"], else: host),
      if(port, do: [":" | Integer.to_string(port)], else: [])
    ]
  end
end

defmodule NewURI do
  def to_string(%{
        host: host,
        authority: authority,
        userinfo: userinfo,
        scheme: scheme,
        port: port,
        path: path,
        query: query,
        fragment: fragment
      }) do
    # Based on https://tools.ietf.org/html/rfc3986#section-5.3
    authority = extract_authority(host, authority, scheme, userinfo, port)

    IO.iodata_to_binary([
      if(scheme, do: [scheme, ?:], else: []),
      if(authority, do: ["//" | authority], else: []),
      if(path, do: path, else: []),
      if(query, do: ["?" | query], else: []),
      if(fragment, do: ["#" | fragment], else: [])
    ])
  end

  defp extract_authority(_host = nil, authority, _scheme, _userinfo, _port) do
    authority
  end

  defp extract_authority(host, _authority, scheme, userinfo, port) do
    [
      if(userinfo, do: [userinfo | "@"], else: []),
      if(String.contains?(host, ":"), do: ["[", host | "]"], else: host),
      if(port && (is_nil(scheme) || URI.default_port(scheme) != port),
        do: [":" | Integer.to_string(port)],
        else: []
      )
    ]
  end
end

inputs = %{
  "Simple URI" => URI.parse("https://elixir-lang.org/getting-started"),
  "URI with default port" => URI.parse("https://elixir-lang.org:443/getting-started"),
  "URI with non-default port" => URI.parse("http://elixir-lang.org:8080/getting-started"),
  "URI with userinfo" => URI.parse("postgres://user:pass@db.example.com:5432/mydb"),
  "URI with IPv6 host" => URI.parse("http://[2001:db8::1]:80/index.html"),
  "URI with query & fragment" => URI.parse("https://google.com/search?q=elixir#result")
}

Benchee.run(
  %{
    "Main (Old) to_string/1" => fn uri -> OldURI.to_string(uri) end,
    "Latest (Optimized) to_string/1" => fn uri -> NewURI.to_string(uri) end
  },
  inputs: inputs,
  time: 2,
  warmup: 1,
  memory_time: 2
)

Benchmark results on my 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: 1 s
time: 2 s
memory time: 2 s
reduction time: 0 ns
parallel: 1
Estimated total run time: 1 min
Excluding outliers: false

##### With input Simple URI #####
Name                                     ips        average  deviation         median         99th %
Latest (Optimized) to_string/1        2.79 M      357.93 ns  ±1897.20%         321 ns         551 ns
Main (Old) to_string/1                2.58 M      387.04 ns  ±1376.56%         351 ns         581 ns

Comparison:
Latest (Optimized) to_string/1        2.79 M
Main (Old) to_string/1                2.58 M - 1.08x slower +29.11 ns

Memory usage statistics:

Name                              Memory usage
Latest (Optimized) to_string/1           312 B
Main (Old) to_string/1                   408 B - 1.31x memory usage +96 B

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

##### With input URI with IPv6 host #####
Name                                     ips        average  deviation         median         99th %
Latest (Optimized) to_string/1        2.66 M      376.57 ns   ±980.21%         360 ns         591 ns
Main (Old) to_string/1                2.46 M      406.81 ns   ±951.47%         381 ns         611 ns

Comparison:
Latest (Optimized) to_string/1        2.66 M
Main (Old) to_string/1                2.46 M - 1.08x slower +30.24 ns

Memory usage statistics:

Name                              Memory usage
Latest (Optimized) to_string/1           360 B
Main (Old) to_string/1                   456 B - 1.27x memory usage +96 B

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

##### With input URI with default port #####
Name                                     ips        average  deviation         median         99th %
Latest (Optimized) to_string/1        2.76 M      361.70 ns  ±1870.83%         331 ns         531 ns
Main (Old) to_string/1                2.56 M      390.72 ns  ±1392.85%         360 ns         591 ns

Comparison:
Latest (Optimized) to_string/1        2.76 M
Main (Old) to_string/1                2.56 M - 1.08x slower +29.02 ns

Memory usage statistics:

Name                              Memory usage
Latest (Optimized) to_string/1           312 B
Main (Old) to_string/1                   408 B - 1.31x memory usage +96 B

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

##### With input URI with non-default port #####
Name                                     ips        average  deviation         median         99th %
Latest (Optimized) to_string/1        2.46 M      406.52 ns  ±1425.70%         371 ns         601 ns
Main (Old) to_string/1                2.36 M      423.44 ns  ±1725.83%         381 ns         631 ns

Comparison:
Latest (Optimized) to_string/1        2.46 M
Main (Old) to_string/1                2.36 M - 1.04x slower +16.92 ns

Memory usage statistics:

Name                              Memory usage
Latest (Optimized) to_string/1           376 B
Main (Old) to_string/1                   376 B - 1.00x memory usage +0 B

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

##### With input URI with query & fragment #####
Name                                     ips        average  deviation         median         99th %
Latest (Optimized) to_string/1        2.50 M      399.44 ns  ±1470.98%         371 ns         602 ns
Main (Old) to_string/1                2.46 M      406.96 ns  ±1131.66%         371 ns         621 ns

Comparison:
Latest (Optimized) to_string/1        2.50 M
Main (Old) to_string/1                2.46 M - 1.02x slower +7.52 ns

Memory usage statistics:

Name                              Memory usage
Latest (Optimized) to_string/1           352 B
Main (Old) to_string/1                   448 B - 1.27x memory usage +96 B

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

##### With input URI with userinfo #####
Name                                     ips        average  deviation         median         99th %
Main (Old) to_string/1                2.82 M      354.24 ns   ±805.43%         341 ns         491 ns
Latest (Optimized) to_string/1        2.80 M      357.48 ns   ±743.11%         341 ns         511 ns

Comparison:
Main (Old) to_string/1                2.82 M
Latest (Optimized) to_string/1        2.80 M - 1.01x slower +3.24 ns

Memory usage statistics:

Name                              Memory usage
Main (Old) to_string/1                   304 B
Latest (Optimized) to_string/1           304 B - 1.00x memory usage +0 B

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

Comment thread lib/elixir/lib/uri.ex Outdated
Co-authored-by: José Valim <jose.valim@gmail.com>
@josevalim
josevalim merged commit 9046a2f into elixir-lang:main Jun 14, 2026
14 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