Skip to content

Optimize Huffman codec - #15675

Open
lorban wants to merge 4 commits into
jetty-12.1.xfrom
enhancement/12.1/wendigo-huffman
Open

Optimize Huffman codec#15675
lorban wants to merge 4 commits into
jetty-12.1.xfrom
enhancement/12.1/wendigo-huffman

Conversation

@lorban

@lorban lorban commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

The Huffman code parts of #15498 made by @wendigo

@lorban lorban self-assigned this Aug 26, 2026
@lorban lorban changed the title Optimize huffman codec Optimize Huffman codec Aug 26, 2026
@lorban lorban moved this to 🏗 In progress in FROZEN Jetty 12.1.13 Aug 26, 2026
lorban and others added 4 commits August 31, 2026 10:57
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
lorban force-pushed the enhancement/12.1/wendigo-huffman branch from c050ada to a551e7b Compare August 31, 2026 08:57
@lorban

lorban commented Aug 31, 2026

Copy link
Copy Markdown
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

Benchmark                           Mode  Cnt     Score     Error  Units
HpackBenchmark.decodeHuffman        avgt    5   654.726 ±   8.337  ns/op
HpackBenchmark.decodeRequest        avgt    5  5908.569 ± 107.024  ns/op
HpackBenchmark.decodeResponse       avgt    5  2414.561 ±  30.869  ns/op
HpackBenchmark.encodeHuffman        avgt    5   348.369 ±   3.070  ns/op
HpackBenchmark.encodeRequest        avgt    5  2844.342 ±  52.615  ns/op
HpackBenchmark.encodeResponse       avgt    5  1223.290 ±  13.642  ns/op
HpackBenchmark.huffmanOctetsNeeded  avgt    5   157.618 ±   0.993  ns/op

with PR

Benchmark                           Mode  Cnt     Score     Error  Units
HpackBenchmark.decodeHuffman        avgt    5   610.991 ±   2.966  ns/op
HpackBenchmark.decodeRequest        avgt    5  5517.073 ± 106.880  ns/op
HpackBenchmark.decodeResponse       avgt    5  2148.981 ±  27.770  ns/op
HpackBenchmark.encodeHuffman        avgt    5   181.198 ±   1.943  ns/op
HpackBenchmark.encodeRequest        avgt    5  2087.576 ±  34.725  ns/op
HpackBenchmark.encodeResponse       avgt    5  1013.702 ±  25.849  ns/op
HpackBenchmark.huffmanOctetsNeeded  avgt    5   125.889 ±   0.216  ns/op

@wendigo

wendigo commented Aug 31, 2026

Copy link
Copy Markdown

@lorban thanks

@sbordet sbordet added the Sponsored This issue affects a user with a commercial support agreement label Aug 31, 2026
@sbordet sbordet moved this to 👀 In review in Jetty 12.1.14 Aug 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Performance Sponsored This issue affects a user with a commercial support agreement

Projects

Status: 👀 In review

Development

Successfully merging this pull request may close these issues.

3 participants