Skip to content

Commit 1cf5cb2

Browse files
authored
Merge pull request #206 from klauspost/flate-fix-huffman-bit-estimates
Flate: Fix/tweak huffman bit estimates
2 parents 81b3ddd + 9ff01f5 commit 1cf5cb2

File tree

9 files changed

+108
-53
lines changed

9 files changed

+108
-53
lines changed

flate/deflate.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -644,21 +644,21 @@ func (d *compressor) init(w io.Writer, level int) (err error) {
644644
d.fill = (*compressor).fillBlock
645645
d.step = (*compressor).store
646646
case level == ConstantCompression:
647-
d.w.logReusePenalty = uint(4)
647+
d.w.logNewTablePenalty = 4
648648
d.window = make([]byte, maxStoreBlockSize)
649649
d.fill = (*compressor).fillBlock
650650
d.step = (*compressor).storeHuff
651651
case level == DefaultCompression:
652652
level = 5
653653
fallthrough
654654
case level >= 1 && level <= 6:
655-
d.w.logReusePenalty = uint(level + 1)
655+
d.w.logNewTablePenalty = 6
656656
d.fast = newFastEnc(level)
657657
d.window = make([]byte, maxStoreBlockSize)
658658
d.fill = (*compressor).fillBlock
659659
d.step = (*compressor).storeFast
660660
case 7 <= level && level <= 9:
661-
d.w.logReusePenalty = uint(level)
661+
d.w.logNewTablePenalty = 10
662662
d.state = &advancedState{}
663663
d.compressionLevel = levels[level]
664664
d.initDeflate()

flate/huffman_bit_writer.go

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -93,12 +93,12 @@ type huffmanBitWriter struct {
9393
err error
9494
lastHeader int
9595
// Set between 0 (reused block can be up to 2x the size)
96-
logReusePenalty uint
97-
lastHuffMan bool
98-
bytes [256]byte
99-
literalFreq [lengthCodesStart + 32]uint16
100-
offsetFreq [32]uint16
101-
codegenFreq [codegenCodeCount]uint16
96+
logNewTablePenalty uint
97+
lastHuffMan bool
98+
bytes [256]byte
99+
literalFreq [lengthCodesStart + 32]uint16
100+
offsetFreq [32]uint16
101+
codegenFreq [codegenCodeCount]uint16
102102

103103
// codegen must have an extra space for the final symbol.
104104
codegen [literalCount + offsetCodeCount + 1]uint8
@@ -119,7 +119,7 @@ type huffmanBitWriter struct {
119119
// If lastHuffMan is set, a table for outputting literals has been generated and offsets are invalid.
120120
//
121121
// An incoming block estimates the output size of a new table using a 'fresh' by calculating the
122-
// optimal size and adding a penalty in 'logReusePenalty'.
122+
// optimal size and adding a penalty in 'logNewTablePenalty'.
123123
// A Huffman table is not optimal, which is why we add a penalty, and generating a new table
124124
// is slower both for compression and decompression.
125125

@@ -349,6 +349,13 @@ func (w *huffmanBitWriter) headerSize() (size, numCodegens int) {
349349
int(w.codegenFreq[18])*7, numCodegens
350350
}
351351

352+
// dynamicSize returns the size of dynamically encoded data in bits.
353+
func (w *huffmanBitWriter) dynamicReuseSize(litEnc, offEnc *huffmanEncoder) (size int) {
354+
size = litEnc.bitLength(w.literalFreq[:]) +
355+
offEnc.bitLength(w.offsetFreq[:])
356+
return size
357+
}
358+
352359
// dynamicSize returns the size of dynamically encoded data in bits.
353360
func (w *huffmanBitWriter) dynamicSize(litEnc, offEnc *huffmanEncoder, extraBits int) (size, numCodegens int) {
354361
header, numCodegens := w.headerSize()
@@ -451,12 +458,12 @@ func (w *huffmanBitWriter) writeDynamicHeader(numLiterals int, numOffsets int, n
451458

452459
i := 0
453460
for {
454-
var codeWord int = int(w.codegen[i])
461+
var codeWord = uint32(w.codegen[i])
455462
i++
456463
if codeWord == badCode {
457464
break
458465
}
459-
w.writeCode(w.codegenEncoding.codes[uint32(codeWord)])
466+
w.writeCode(w.codegenEncoding.codes[codeWord])
460467

461468
switch codeWord {
462469
case 16:
@@ -602,14 +609,14 @@ func (w *huffmanBitWriter) writeBlockDynamic(tokens *tokens, eof bool, input []b
602609
var size int
603610
// Check if we should reuse.
604611
if w.lastHeader > 0 {
605-
// Estimate size for using a new table
612+
// Estimate size for using a new table.
613+
// Use the previous header size as the best estimate.
606614
newSize := w.lastHeader + tokens.EstimatedBits()
615+
newSize += newSize >> w.logNewTablePenalty
607616

608617
// The estimated size is calculated as an optimal table.
609618
// We add a penalty to make it more realistic and re-use a bit more.
610-
newSize += newSize >> (w.logReusePenalty & 31)
611-
extra := w.extraBitSize()
612-
reuseSize, _ := w.dynamicSize(w.literalEncoding, w.offsetEncoding, extra)
619+
reuseSize := w.dynamicReuseSize(w.literalEncoding, w.offsetEncoding) + w.extraBitSize()
613620

614621
// Check if a new table is better.
615622
if newSize < reuseSize {
@@ -801,21 +808,30 @@ func (w *huffmanBitWriter) writeBlockHuff(eof bool, input []byte, sync bool) {
801808
}
802809

803810
// Add everything as literals
804-
estBits := histogramSize(input, w.literalFreq[:], !eof && !sync) + 15
811+
// We have to estimate the header size.
812+
// Assume header is around 70 bytes:
813+
// https://stackoverflow.com/a/25454430
814+
const guessHeaderSizeBits = 70 * 8
815+
estBits, estExtra := histogramSize(input, w.literalFreq[:], !eof && !sync)
816+
estBits += w.lastHeader + 15
817+
if w.lastHeader == 0 {
818+
estBits += guessHeaderSizeBits
819+
}
820+
estBits += estBits >> w.logNewTablePenalty
805821

806822
// Store bytes, if we don't get a reasonable improvement.
807823
ssize, storable := w.storedSize(input)
808-
if storable && ssize < (estBits+estBits>>4) {
824+
if storable && ssize < estBits {
809825
w.writeStoredHeader(len(input), eof)
810826
w.writeBytes(input)
811827
return
812828
}
813829

814830
if w.lastHeader > 0 {
815-
size, _ := w.dynamicSize(w.literalEncoding, huffOffset, w.lastHeader)
816-
estBits += estBits >> (w.logReusePenalty)
831+
reuseSize := w.literalEncoding.bitLength(w.literalFreq[:256])
832+
estBits += estExtra
817833

818-
if estBits < size {
834+
if estBits < reuseSize {
819835
// We owe an EOB
820836
w.writeCode(w.literalEncoding.codes[endBlockMarker])
821837
w.lastHeader = 0

flate/huffman_bit_writer_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ func TestBlockHuff(t *testing.T) {
3333
if strings.HasSuffix(in, ".in") {
3434
out = in[:len(in)-len(".in")] + ".golden"
3535
}
36-
testBlockHuff(t, in, out)
36+
t.Run(in, func(t *testing.T) {
37+
testBlockHuff(t, in, out)
38+
})
3739
}
3840
}
3941

@@ -45,6 +47,7 @@ func testBlockHuff(t *testing.T, in, out string) {
4547
}
4648
var buf bytes.Buffer
4749
bw := newHuffmanBitWriter(&buf)
50+
bw.logNewTablePenalty = 8
4851
bw.writeBlockHuff(false, all, false)
4952
bw.flush()
5053
got := buf.Bytes()

flate/huffman_code.go

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -320,26 +320,44 @@ func (h *huffmanEncoder) generate(freq []uint16, maxBits int32) {
320320
h.assignEncodingAndSize(bitCount, list)
321321
}
322322

323+
func atLeastOne(v float32) float32 {
324+
if v < 1 {
325+
return 1
326+
}
327+
return v
328+
}
329+
323330
// histogramSize accumulates a histogram of b in h.
324331
// An estimated size in bits is returned.
325332
// Unassigned values are assigned '1' in the histogram.
326333
// len(h) must be >= 256, and h's elements must be all zeroes.
327-
func histogramSize(b []byte, h []uint16, fill bool) int {
334+
func histogramSize(b []byte, h []uint16, fill bool) (int, int) {
328335
h = h[:256]
329336
for _, t := range b {
330337
h[t]++
331338
}
332-
invTotal := 1.0 / float64(len(b))
333-
shannon := 0.0
334-
single := math.Ceil(-math.Log2(invTotal))
335-
for i, v := range h[:] {
336-
if v > 0 {
337-
n := float64(v)
338-
shannon += math.Ceil(-math.Log2(n*invTotal) * n)
339-
} else if fill {
340-
shannon += single
341-
h[i] = 1
339+
invTotal := 1.0 / float32(len(b))
340+
shannon := float32(0.0)
341+
var extra float32
342+
if fill {
343+
oneBits := atLeastOne(-mFastLog2(invTotal))
344+
for i, v := range h[:] {
345+
if v > 0 {
346+
n := float32(v)
347+
shannon += atLeastOne(-mFastLog2(n*invTotal)) * n
348+
} else {
349+
h[i] = 1
350+
extra += oneBits
351+
}
352+
}
353+
} else {
354+
for _, v := range h[:] {
355+
if v > 0 {
356+
n := float32(v)
357+
shannon += atLeastOne(-mFastLog2(n*invTotal)) * n
358+
}
342359
}
343360
}
344-
return int(shannon + 0.99)
361+
362+
return int(shannon + 0.99), int(extra + 0.99)
345363
}
-14 Bytes
Binary file not shown.
-7 Bytes
Binary file not shown.

flate/testdata/huffman-text.golden

-35 Bytes
Binary file not shown.

flate/token.go

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -184,9 +184,7 @@ func (t *tokens) indexTokens(in []token) {
184184
t.Reset()
185185
for _, tok := range in {
186186
if tok < matchType {
187-
t.tokens[t.n] = tok
188-
t.litHist[tok]++
189-
t.n++
187+
t.AddLiteral(tok.literal())
190188
continue
191189
}
192190
t.AddMatch(uint32(tok.length()), tok.offset())
@@ -211,43 +209,53 @@ func (t *tokens) AddLiteral(lit byte) {
211209
t.nLits++
212210
}
213211

212+
// from https://stackoverflow.com/a/28730362
213+
func mFastLog2(val float32) float32 {
214+
ux := int32(math.Float32bits(val))
215+
log2 := (float32)(((ux >> 23) & 255) - 128)
216+
ux &= -0x7f800001
217+
ux += 127 << 23
218+
uval := math.Float32frombits(uint32(ux))
219+
log2 += ((-0.34484843)*uval+2.02466578)*uval - 0.67487759
220+
return log2
221+
}
222+
214223
// EstimatedBits will return an minimum size estimated by an *optimal*
215224
// compression of the block.
216225
// The size of the block
217226
func (t *tokens) EstimatedBits() int {
218-
shannon := float64(0)
227+
shannon := float32(0)
219228
bits := int(0)
220229
nMatches := 0
221230
if t.nLits > 0 {
222-
invTotal := 1.0 / float64(t.nLits)
231+
invTotal := 1.0 / float32(t.nLits)
223232
for _, v := range t.litHist[:] {
224233
if v > 0 {
225-
n := float64(v)
226-
shannon += math.Ceil(-math.Log2(n*invTotal) * n)
234+
n := float32(v)
235+
shannon += -mFastLog2(n*invTotal) * n
227236
}
228237
}
229238
// Just add 15 for EOB
230239
shannon += 15
231-
for _, v := range t.extraHist[1 : literalCount-256] {
240+
for i, v := range t.extraHist[1 : literalCount-256] {
232241
if v > 0 {
233-
n := float64(v)
234-
shannon += math.Ceil(-math.Log2(n*invTotal) * n)
235-
bits += int(lengthExtraBits[v&31]) * int(v)
242+
n := float32(v)
243+
shannon += -mFastLog2(n*invTotal) * n
244+
bits += int(lengthExtraBits[i&31]) * int(v)
236245
nMatches += int(v)
237246
}
238247
}
239248
}
240249
if nMatches > 0 {
241-
invTotal := 1.0 / float64(nMatches)
242-
for _, v := range t.offHist[:offsetCodeCount] {
250+
invTotal := 1.0 / float32(nMatches)
251+
for i, v := range t.offHist[:offsetCodeCount] {
243252
if v > 0 {
244-
n := float64(v)
245-
shannon += math.Ceil(-math.Log2(n*invTotal) * n)
246-
bits += int(offsetExtraBits[v&31]) * int(n)
253+
n := float32(v)
254+
shannon += -mFastLog2(n*invTotal) * n
255+
bits += int(offsetExtraBits[i&31]) * int(v)
247256
}
248257
}
249258
}
250-
251259
return int(shannon) + bits
252260
}
253261

flate/token_test.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package flate
22

33
import (
4+
"bytes"
45
"io/ioutil"
56
"testing"
67
)
@@ -27,8 +28,17 @@ func loadTestTokens(t testFatal) *tokens {
2728
func Test_tokens_EstimatedBits(t *testing.T) {
2829
tok := loadTestTokens(t)
2930
// The estimated size, update if method changes.
30-
const expect = 199380
31-
if n := tok.EstimatedBits(); n != expect {
31+
const expect = 221057
32+
n := tok.EstimatedBits()
33+
var buf bytes.Buffer
34+
wr := newHuffmanBitWriter(&buf)
35+
wr.writeBlockDynamic(tok, true, nil, true)
36+
if wr.err != nil {
37+
t.Fatal(wr.err)
38+
}
39+
wr.flush()
40+
t.Log("got:", n, "actual:", buf.Len()*8, "(header not part of estimate)")
41+
if n != expect {
3242
t.Error("want:", expect, "bits, got:", n)
3343
}
3444
}

0 commit comments

Comments
 (0)