Optimize HPack - #15674
Open
lorban wants to merge 11 commits into
Open
Conversation
A h2c connection spends most of its per request time parsing frame headers and encoding and decoding HPACK, and there was no benchmark covering any of it, so there was no way to tell whether a change to that path was an improvement. Cover frame header and preface parsing, HPACK encoding and decoding of a realistic request and response, and the pieces they are built from: Huffman encoding and decoding, field name and value validation, and ASCII lowercasing.
Signed-off-by: Ludovic Orban <lorban@bitronix.be>
The existing HPACK benchmarks build a fresh encoder and decoder per invocation, which measures the first request on a connection, where every field is a literal. On a long lived connection the dynamic table absorbs the repeated fields: the same request that encodes to 392 octets cold encodes to 12 once the table is warm, so the Huffman coding that dominates the cold path barely runs. Add an encoder and decoder pair warmed the way a connection warms them, so both ends of the range are covered. The decoder is fed exactly the sequence the encoder produced, otherwise its dynamic table would not match. The two paths have quite different profiles, and the steady state one is the better guide for a server holding connections open.
lorban
force-pushed
the
enhancement/12.1/wendigo-hpack
branch
2 times, most recently
from
August 31, 2026 07:40
54e1931 to
3baf263
Compare
Signed-off-by: Ludovic Orban <lorban@bitronix.be>
isLegalH2H3FieldName looked up TOKENS[c] per character, which is an array load followed by a dereference of the Token object and a switch on its type; both validators are run for every field on every HPACK encode and decode. isLegalFieldValue additionally re-evaluated the "is this the first or last character" test on every iteration. Classify all 256 ISO-8859-1 characters once, into a flat byte array of bit flags derived from TOKENS, so a character costs one load and one mask. Because the flags of a whole string can be accumulated with &, legality becomes a single test at the end rather than one per character, and the first/last field-vchar test moves out of the loop. Verified against the previous implementations over all 65536 characters and 1.2M strings, which agree in every case.
An entry of the HPACK dynamic table is referenced by index by every message that uses it, and each reference re-derived whether its name and value were legal, which is a full scan of both. On a connection in steady state, where the table absorbs the repeated fields, this was the single largest cost of decoding a message. Memoize the outcome on the entry. The field is immutable, so it cannot change; the memo races benignly, as the field hash in HttpField already does, and HpackContext is single threaded anyway. The outcome is remembered rather than assumed. A field that fails validation is still added to the dynamic table, because the failure only fails its own stream, so a peer could otherwise smuggle an illegal value into the table on a stream it is willing to lose and then reference it freely. Replaying the remembered outcome keeps every later stream that references the entry failing, exactly as before. Verified against the previous implementation with that smuggling sequence: both fail the inserting stream and every later stream that references the entry by index, with the same message. Decoding a request in steady state drops 52%.
Encoding a message walked the field list three times: once to verify every field could be encoded, once inside getCSV() to collect the hop headers named by the Connection header, and once to encode. The first two do not write anything, so they merge into a single pass. That pass also memoizes validation on the dynamic table entry, as the decoder now does. A field already in the table passed the same check when it was added, and re-deriving it is a scan of the name and of the value on every message. Static entries are still checked normally: a field an application named after a pseudo header matches a static entry but must still be rejected. The outcome is memoized rather than assumed, so turning validation off, admitting an illegal field to the table, and turning it back on still rejects it. The lookup is skipped while the dynamic table is empty, where it could only miss; that keeps the first message on a connection, which has nothing to gain here, from paying for it. Verified against the previous implementation across legal fields, pseudo header names, illegal values, repeated encodes that exercise the memo, and the validation off then on sequence: identical in every case. Encoding a request in steady state drops 52%.
An ISO-8859-1 String is byte for byte its encoded bytes, but both decoders built one up by appending one character at a time to a StringBuilder: a bounds check, a position update and a capacity check per octet, for every literal header name and value. HpackDecoder.toISO88591String now decodes straight from the buffer, using the backing array when there is one, so that String construction is a single array copy. The explicit remaining() check preserves the BufferUnderflowException thrown by the previous per-octet reads, which the array path would otherwise not raise. NBitStringDecoder accumulates into a byte array that grows as needed, since it must handle strings split across buffers; the array starts small rather than at the declared length, so a large declared length does not allocate up front.
Signed-off-by: Ludovic Orban <lorban@bitronix.be>
Signed-off-by: Ludovic Orban <lorban@bitronix.be>
…ck with isEmpty() Signed-off-by: Ludovic Orban <lorban@bitronix.be>
lorban
force-pushed
the
enhancement/12.1/wendigo-hpack
branch
from
August 31, 2026 07:44
3baf263 to
1262b32
Compare
Contributor
Author
|
I've cleaned up the micro benchmark, and a few nits here and there (I had to modify how 12.1.x vanilla with PR: |
|
@lorban thanks :) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The HPack parts of #15498 made by @wendigo