Skip to content

Compare header names case-insensitively without allocating - #49

Merged
hellerve merged 1 commit into
masterfrom
perf/header-lookup
Oct 3, 2026
Merged

hellerve merged 1 commit into
masterfrom
perf/header-lookup

Conversation

@hellerve

@hellerve hellerve commented Oct 3, 2026

Copy link
Copy Markdown
Member

header-lookup and header-values lower-cased every request header name on every call: String.ascii-to-lower on each key, so two allocations per header per lookup. Precondition.evaluate makes up to four lookups, and Request.header and if-range-matches? make more, so a plain GET paid this many times over.

This compares names case-insensitively in place instead: lengths first, then byte by byte with A–Z folded to a–z, the same folding tolower does in the C locale. It allocates nothing.

Behaviour is unchanged. Old against new, header-lookup and header-values both, gave 0 divergences over 135,137 queries. The header maps were built from every name of up to three bytes over a, A, z, Z, -, @, [, ` and {, which are the bytes on either side of both letter ranges, plus two names with bytes above 127. The check catches a planted bug at either end of the folding range: not folding Z gives 707 divergences, folding [ gives 976.

Timing. Median of three runs, 200,000 calls each, on an M1 Max, against the headers of a typical browser GET (nine headers):

case level master this branch speedup
header-lookup, header absent -O0 1,553 ns 542 ns 2.9×
-O1 576 52 11.1×
-O2 574 50 11.6×
-O3 548 27 20.4×
-Os 605 84 7.2×
--optimize 554 28 19.4×
header-values, header present -O0 1,736 1,221 1.4×
-O1 658 155 4.2×
-O2 650 156 4.2×
-O3 640 156 4.1×
-Os 674 180 3.7×
--optimize 644 154 4.2×

Through web 0.12.0's web-build-response, from -O1 up, a build with this change and carpentry-org/time#33's strptime change is 0.4–0.5 µs faster on a plain GET and 0.1–0.2 µs faster on an If-Modified-Since request than one without them. The two changes were not measured apart there.

Tests. Run with -Werror: 516 passed, 0 failed. No test changed. angler and carp-fmt --check, both built from their current HEADs, are clean. carp -x gendocs.carp succeeds.

Request.parse still lower-cases each header name once, to find Cookie. It is not a lookup and is left alone here.

@hellerve
hellerve merged commit c11a31d into master Oct 3, 2026
2 checks passed
@hellerve
hellerve deleted the perf/header-lookup branch October 3, 2026 21:23
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.

1 participant