Skip to content

perf(header): skip the 12-slot step when a HeaderMap first grows - #886

Closed
patrick-immometrica wants to merge 1 commit into
hyperium:masterfrom
patrick-immometrica:perf-header-map-initial-capacity
Closed

patrick-immometrica wants to merge 1 commit into
hyperium:masterfrom
patrick-immometrica:perf-header-map-initial-capacity

Conversation

@patrick-immometrica

Copy link
Copy Markdown

HeaderMap::new() doesn't allocate. The first insert reserves 8 indices (6 usable), and every grow after that doubles. A request or response with 13-24 headers, which is normal for browsers and most proxied traffic, rehashes twice on the way: 6 -> 12 -> 24. h2 decodes every header block into a fresh HeaderMap::new(), so it pays this on every request.

This makes the first grow go from 8 to 32 raw slots, i.e. 6 -> 24 usable. Maps of up to 6 headers are unchanged, and so is everything after 24.

I counted instructions with cachegrind (Linux, rust:1 container). The workload appends n standard headers into HeaderMap::new() 10k times; the numbers are per map, with the empty loop subtracted.

headers master this PR change
1 525 525 0
6 1395 1395 0
8 2748 2763 +0.5%
12 3621 3478 -4%
16 5602 4272 -24%
20 6355 5025 -21%

Criterion wall-clock on macOS was in line with this: -21% for 16 and 20 headers. My machine was busy, so I trust the instruction counts more.

Allocations per map:

headers master this PR
1-6 2 allocs, 656 B same
7-12 4 allocs, 1968 B 4 allocs, 3280 B
13-24 6 allocs, 4592 B 4 allocs, 3280 B

The cost is memory for maps of 7-12 headers, which now hold room for 24. I also tried starting at 16 slots, but that made small maps 20-55% slower and doubled their size, so I went with this.

I added a test that checks the capacity after the first grow. It fails on master with left: 12, right: 24.

A map from HeaderMap::new() starts with 6 usable slots and doubles
from there, so a header block of 13-24 entries, which is common for
browser requests, rehashes twice: 6 -> 12 -> 24. Going straight from
8 to 32 raw slots on the first grow makes that a single rehash and
saves a pair of allocations. Maps of up to 6 entries are untouched.
@patrick-immometrica
patrick-immometrica marked this pull request as ready for review November 5, 2026 11:09
@seanmonstar

Copy link
Copy Markdown
Member

Thanks for the PR!

My gut tells me to not special case in the container here, but to instead keep a WMA in a parser to use to reserve the initial amount, wherever that is needed.

@patrick-immometrica

Copy link
Copy Markdown
Author

Makes sense. h2 is where this hurts, so I moved it into h2's frame decoder as a moving average: hyperium/h2#999. Closing this one.

@patrick-immometrica
patrick-immometrica deleted the perf-header-map-initial-capacity branch November 5, 2026 17:28
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