Skip to content

perf: reduce CPU overhead in gzip decompression and header lowercasing - #17

Closed
burruplambert wants to merge 3 commits into
bogdanfinn:masterfrom
burruplambert:master
Closed

perf: reduce CPU overhead in gzip decompression and header lowercasing#17
burruplambert wants to merge 3 commits into
bogdanfinn:masterfrom
burruplambert:master

Conversation

@burruplambert

@burruplambert burruplambert commented Jul 1, 2026

Copy link
Copy Markdown

Summary

  • Replace stdlib compress/gzip and compress/flate with github.com/klauspost/compress - drop-in replacement with a faster Huffman decoder. Measured ~5% total CPU reduction in production profiling of a high-throughput HTTP client workload.

  • Eliminate per-comparison strings.ToLower in headerSorter.Less - the sort comparator was calling strings.ToLower on every comparison, causing O(n log n) allocations per request. Pre-compute lowercase keys once before sorting and swap them in parallel with the key-value pairs.

  • Use lowerHeader map lookup in encodeHeaders and encodeTrailers- replace strings.ToLower with the existing lowerHeader function in http2/headermap.go, which does a zero-allocation map lookup for the ~40 most common HTTP headers before falling back to strings.ToLower.

Combined measured savings: ~7% total CPU in production profiling under sustained high-throughput HTTP/2 traffic.

Changes

  • transport.go - swap compress/gzip and compress/flate imports to github.com/klauspost/compress
  • header.go - add lowerKeys field to headerSorter, pre-compute in SortedKeyValuesBy, swap in Swap, use in Less
  • http2/transport.go - replace strings.ToLower(name) with lowerHeader(name) in encodeHeaders and encodeTrailers

The stdlib compress/gzip and compress/flate use a slower Huffman
decoder. klauspost/compress is a drop-in replacement with a faster
implementation, measured at ~5% total CPU reduction in production
profiling of a high-throughput HTTP client workload.
…encoding

headerSorter.Less called strings.ToLower on every comparison during
sort, causing O(n log n) allocations per request. Pre-compute lowercase
keys once before sorting and swap them in parallel with the key-value
pairs.

In http2 encodeHeaders and encodeTrailers, replace strings.ToLower with
lowerHeader which uses the existing common header map for zero-alloc
lookups on standard headers.

Combined savings: ~2% total CPU in production profiling.
@burruplambert

burruplambert commented Jul 28, 2026

Copy link
Copy Markdown
Author

Pushed a fix for a bug in the original version of this PR: Swap swapped lowerKeys unconditionally, but SortedKeyValues (the path taken when no Header-Order: key is set) never populates it, so any orderless header write panicked with an index out of range. This is reachable from Header.Write/WriteSubset and the http2 enumerateHeaders fallback, and it is also what made TestHeaderWrite panic on this branch.

The fix guards the lowerKeys swap and resets order/lowerKeys on sorters reused from the pool. Resetting order also fixes a pre-existing issue where a pooled sorter reused by SortedKeyValues could sort by a stale order map from a previous SortedKeyValuesBy call. That one exists on master today independent of this PR, so I opened #18 with a standalone fix. Regression tests added for both.

Note: TestHeaderWrite case #9 (mixed-case Header-Order: values) fails identically on current master - the order map in writeSubset is built with the values verbatim while the sort looks up lowercased keys - so that failure is pre-existing and unrelated to this PR.

SortedKeyValues sorts without populating lowerKeys, but Swap swapped
lowerKeys unconditionally, panicking with index out of range for any
header map without a Header-Order key (reachable from Header.Write,
WriteSubset, and the http2 enumerateHeaders fallback).

Guard the lowerKeys swap and reset order/lowerKeys on sorters reused
from the pool, so a sorter previously used by SortedKeyValuesBy cannot
leak a stale order (or stale lowered keys) into an orderless sort.

Adds regression tests for both the panic and the pool-reuse case.
@burruplambert

Copy link
Copy Markdown
Author

Closing to split this into focused PRs, since it bundled two unrelated changes:

Nothing here had been reviewed yet, so no review context is lost by the split.

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