Optimize Huffman codec - #15675
Open
lorban wants to merge 4 commits into
Open
Conversation
Signed-off-by: Ludovic Orban <lorban@bitronix.be>
Encoding looked up table[c][0] and table[c][1] in an int[][], which is a load of the row reference followed by a load of the element, twice per character, with the rows scattered on the heap. Pack each code and its bit length into a single long, (code << 5) | bits, held in a flat array, so a character costs one load from one contiguous table. Codes are at most 30 bits, so the length fits in 5 bits and the packed value fits in a long. The bit accumulator was also drained one octet per put as soon as 8 bits were pending. Since codes are at most 30 bits, up to 31 bits can accumulate while keeping the accumulator within 64 bits, which lets 4 octets be written with a single putInt. The octets are written in the same order, as the buffer is big endian. Any remaining octets are drained, and padded, as before. Verified by the RFC 7541 encoding vectors in HuffmanTest and by round tripping 200k random strings over the whole ISO-8859-1 range, decoded both in bulk and one octet at a time.
Huffman decoding is the single hottest part of HPACK decoding, at around 40% of the time to decode a realistic request. Each symbol cost 3 loads: one from the flattened tree to find the node, then one each from rowbits and rowsym to read the code length and the symbol. 74 of the 95 printable ASCII characters have a code of 8 bits or fewer, so nearly every symbol of real header text is a terminal node reached directly from the root with the 8 bits already in hand. Precompute those 256 root entries into a table holding the symbol and its length packed as (bits << 8) | symbol, which decodes them with a single load from 512 bytes that stay in the innermost cache. A zero entry means the code is longer than 8 bits, and falls back to walking the tree. EOS is a 30 bit code so it never appears in the table, and its check on the tree walk is unaffected. Huffman decode drops 11%, and decoding a request 5%.
A Huffman string is written as its encoded length followed by the content, and the length was computed by octetsNeeded(), a full pass over the string doing the same table lookups the encoding pass then repeats. It was around 10% of the time to encode a request. Encode the content first, into the space after the octet the length is written into, then fill the length in. That octet only has room for a length below the prefix maximum, which every string encoding to fewer than 127 octets satisfies for the 7 bit prefix used by HPACK; for a longer string the buffer is rewound and the length and content are written the direct way, so no octets are ever shifted. octetsNeeded() stays for the callers that size a buffer up front. Verified byte for byte against the previous implementation over 186k encodings: every prefix from 1 to 8, both huffman flags, several leading octets, and lengths spanning the boundary where the rewind path takes over, including strings of control characters whose codes are long enough to force it. Encoding a request drops 26%, a response 13%. Signed-off-by: Ludovic Orban <lorban@bitronix.be>
lorban
force-pushed
the
enhancement/12.1/wendigo-huffman
branch
from
August 31, 2026 08:57
c050ada to
a551e7b
Compare
Contributor
Author
|
I've cleaned up the micro benchmark, but this looks like another set of optimizations that do improve perf: 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 Huffman code parts of #15498 made by @wendigo