encode: reduce allocations in AcceptedEncodings (#7847)

AcceptedEncodings runs for every request when response compression is enabled,
so its allocations contribute directly to per-request garbage and GC pressure.
While the gains are modest, this cuts allocations by two-thirds and bytes
nearly in half on a hot path that executes on every compressed request. Fewer
allocations means less work for the garbage collector, which at high request
rates translates into lower GC frequency and steadier tail latency, not just
faster execution of this one function in isolation.

For each token in the Accept-Encoding header it called strings.Split(accepted,
";"), allocating a throwaway slice per encoding, and grew the prefs slice from
empty. This commit replaces the per-token Split with strings.Cut (zero
allocation) and presize prefs from the comma count.

As requested per the policy, this commit adds a benchmark to exercise the
change. With header "gzip, deflate, br;q=0.9, zstd;q=0.8":

AcceptedEncodings
- sec/op  1158.5n -> 771.8n  (-33.38%)
- B/op 408 -> 216  (-47.06%)
- allocs/op 9 -> 3  (-66.67%)

Co-authored-by: @dunglas
This commit is contained in:
Julien Voisin 2026-07-31 16:23:57 +02:00 committed by GitHub
parent 54a6b8bc9e
commit 323e3fe4b7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 24 additions and 6 deletions

View file

@ -0,0 +1,17 @@
package encode
import (
"net/http"
"net/http/httptest"
"testing"
)
func BenchmarkAcceptedEncodings(b *testing.B) {
r := httptest.NewRequest(http.MethodGet, "/", nil)
r.Header.Set("Accept-Encoding", "gzip, deflate, br;q=0.9, zstd;q=0.8")
prefer := []string{"zstd", "br", "gzip"}
b.ReportAllocs()
for b.Loop() {
_ = AcceptedEncodings(r, prefer)
}
}

View file

@ -504,17 +504,18 @@ func AcceptedEncodings(r *http.Request, preferredOrder []string) []string {
}
websocketKey := r.Header.Get("Sec-Websocket-Key")
prefs := []encodingPreference{}
prefs := make([]encodingPreference, 0, strings.Count(acceptEncHeader, ",")+1)
for accepted := range strings.SplitSeq(acceptEncHeader, ",") {
parts := strings.Split(accepted, ";")
encName := strings.ToLower(strings.TrimSpace(parts[0]))
encName, params, found := strings.Cut(accepted, ";")
encName = strings.ToLower(strings.TrimSpace(encName))
// determine q-factor
qFactor := 1.0
if len(parts) > 1 {
qFactorStr := strings.ToLower(strings.TrimSpace(parts[1]))
if strings.HasPrefix(qFactorStr, "q=") {
if found {
qFactorStr, _, _ := strings.Cut(params, ";")
qFactorStr = strings.TrimSpace(qFactorStr)
if len(qFactorStr) >= 2 && strings.EqualFold(qFactorStr[:2], "q=") {
if qFactorFloat, err := strconv.ParseFloat(qFactorStr[2:], 32); err == nil {
if qFactorFloat >= 0 && qFactorFloat <= 1 {
qFactor = qFactorFloat