Skip to content

perf: speed up percent_decode using bulk run-copying - #1202

Merged
anonrig merged 3 commits into
ada-url:mainfrom
jbergstroem:chore/decode_performance_improvement
Jul 31, 2026
Merged

perf: speed up percent_decode using bulk run-copying#1202
anonrig merged 3 commits into
ada-url:mainfrom
jbergstroem:chore/decode_performance_improvement

Conversation

@jbergstroem

@jbergstroem jbergstroem commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Speed up ada::unicode::percent_decode, the routine that decodes %XX escapes when parsing hosts (via to_ascii) and search-param values.

The decoded output is never longer than the input, size the result buffer once and copy plain runs in bulk; the same approach form_urlencoded_decode already uses.

What changed

  • Allocate the output once at input.size() and shrink to the final length at the end, instead of reserve + per-byte +=.
  • Plain (non-%) spans are located with std::memchr('%') and copied with a single std::memcpy, rather than appending one character at a time.
  • %XX runs are decoded through the existing unhex_table (two loads + an OR to validate), which is above the function so both percent_decode and form_urlencoded_decode share it.
  • Add benchmarks/percent_decode.cpp, mirroring percent_encode.cpp.
  • Increase coverage in the test suite to pick up introduced branches.

Benchmark results

Measured with a Apple M5 (benchmarks/percent_decode, --benchmark_min_time=1s):

Benchmark Before After Improvement
Decode time/url ~54.3 ns ~36.4 ns 1.49×
Decode throughput ~1.03 GB/s ~1.54 GB/s 1.49×

Testing

  • Full test suite passes
  • clang-format, clang-tidy introduces no new findings
  • Verified output to be byte-identical to the previous implementation over fuzzed inputs plus a few hand-picked edge cases (%, %2, %%41,%g0, trailing %, doubly-encoded, some utf-8 strings)

Edit: side-note, just noticed #1124 which might be a longer term solution; feel free to close this if thats the preferred path.

Rewrite `percent_decode` to write into a single pre-sized buffer, copying plain
runs in bulk via `memchr('%')` and decoding `%XX` escapes through the branchless
`unhex_table`, then shrinking to the final length. This replaces the byte-at-a-time
append loop and mirrors the approach already used by `form_urlencoded_decode`.
Decoding never grows the input, so the output buffer is sized once up front.

Add `benchmarks/percent_decode.cpp` covering sparse, densely-encoded, long-plain-run, and utf-8 inputs.

~1.48x faster on the new benchmark (54.3 -> 36.4 ns/url; 1.03 -> 1.54 GB/s).
@anonrig
anonrig requested a review from lemire July 31, 2026 17:12
@anonrig

anonrig commented Jul 31, 2026

Copy link
Copy Markdown
Member

Nice work!

@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.59259% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 61.14%. Comparing base (d154358) to head (f6cbe34).

Files with missing lines Patch % Lines
src/unicode.cpp 92.59% 0 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1202      +/-   ##
==========================================
+ Coverage   61.06%   61.14%   +0.08%     
==========================================
  Files          38       38              
  Lines        6939     6947       +8     
  Branches     3231     3231              
==========================================
+ Hits         4237     4248      +11     
+ Misses        749      748       -1     
+ Partials     1953     1951       -2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread src/unicode.cpp
@anonrig

anonrig commented Jul 31, 2026

Copy link
Copy Markdown
Member

Edit: side-note, just noticed #1124 which might be a longer term solution; feel free to close this if thats the preferred path.

It has regressions... That's why we couldn't land it.

@anonrig
anonrig enabled auto-merge (squash) July 31, 2026 18:18
@anonrig
anonrig merged commit d0d70e3 into ada-url:main Jul 31, 2026
54 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants