Skip to content

Commit

Permalink
Reduce allocations in flate decompressor and minor code improvements (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
flrdv authored Oct 10, 2023
1 parent 2d59657 commit c051ef1
Show file tree
Hide file tree
Showing 4 changed files with 78 additions and 41 deletions.
10 changes: 5 additions & 5 deletions flate/_gen/gen_inflate.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ readLiteral:
dict.writeByte(byte(v))
if dict.availWrite() == 0 {
f.toRead = dict.readFlush()
f.step = (*decompressor).$FUNCNAME$
f.step = $FUNCNAME$
f.stepState = stateInit
f.b, f.nb = fb, fnb
return
Expand Down Expand Up @@ -275,7 +275,7 @@ copyHistory:
if dict.availWrite() == 0 || f.copyLen > 0 {
f.toRead = dict.readFlush()
f.step = (*decompressor).$FUNCNAME$ // We need to continue this work
f.step = $FUNCNAME$ // We need to continue this work
f.stepState = stateDict
f.b, f.nb = fb, fnb
return
Expand All @@ -291,13 +291,13 @@ copyHistory:
s = strings.Replace(s, "$TYPE$", t, -1)
f.WriteString(s)
}
f.WriteString("func (f *decompressor) huffmanBlockDecoder() func() {\n")
f.WriteString("func (f *decompressor) huffmanBlockDecoder() {\n")
f.WriteString("\tswitch f.r.(type) {\n")
for i, t := range types {
f.WriteString("\t\tcase " + t + ":\n")
f.WriteString("\t\t\treturn f.huffman" + names[i] + "\n")
f.WriteString("\t\t\tf.huffman" + names[i] + "()\n")
}
f.WriteString("\t\tdefault:\n")
f.WriteString("\t\t\treturn f.huffmanGenericReader\n")
f.WriteString("\t\t\tf.huffmanGenericReader()\n")
f.WriteString("\t}\n}\n")
}
66 changes: 51 additions & 15 deletions flate/inflate.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,9 @@ func (h *huffmanDecoder) init(lengths []int) bool {
const sanity = false

if h.chunks == nil {
h.chunks = &[huffmanNumChunks]uint16{}
h.chunks = new([huffmanNumChunks]uint16)
}

if h.maxRead != 0 {
*h = huffmanDecoder{chunks: h.chunks, links: h.links}
}
Expand Down Expand Up @@ -175,6 +176,7 @@ func (h *huffmanDecoder) init(lengths []int) bool {
}

h.maxRead = min

chunks := h.chunks[:]
for i := range chunks {
chunks[i] = 0
Expand Down Expand Up @@ -202,8 +204,7 @@ func (h *huffmanDecoder) init(lengths []int) bool {
if cap(h.links[off]) < numLinks {
h.links[off] = make([]uint16, numLinks)
} else {
links := h.links[off][:0]
h.links[off] = links[:numLinks]
h.links[off] = h.links[off][:numLinks]
}
}
} else {
Expand Down Expand Up @@ -277,14 +278,26 @@ func (h *huffmanDecoder) init(lengths []int) bool {
return true
}

// The actual read interface needed by NewReader.
// Reader is the actual read interface needed by NewReader.
// If the passed in io.Reader does not also have ReadByte,
// the NewReader will introduce its own buffering.
type Reader interface {
io.Reader
io.ByteReader
}

type step uint8

const (
copyData step = iota + 1
nextBlock
huffmanBytesBuffer
huffmanBytesReader
huffmanBufioReader
huffmanStringsReader
huffmanGenericReader
)

// Decompress state.
type decompressor struct {
// Input source.
Expand All @@ -303,7 +316,7 @@ type decompressor struct {

// Next step in the decompression,
// and decompression state.
step func(*decompressor)
step step
stepState int
err error
toRead []byte
Expand Down Expand Up @@ -342,7 +355,7 @@ func (f *decompressor) nextBlock() {
// compressed, fixed Huffman tables
f.hl = &fixedHuffmanDecoder
f.hd = nil
f.huffmanBlockDecoder()()
f.huffmanBlockDecoder()
if debugDecode {
fmt.Println("predefinied huffman block")
}
Expand All @@ -353,7 +366,7 @@ func (f *decompressor) nextBlock() {
}
f.hl = &f.h1
f.hd = &f.h2
f.huffmanBlockDecoder()()
f.huffmanBlockDecoder()
if debugDecode {
fmt.Println("dynamic huffman block")
}
Expand All @@ -379,14 +392,16 @@ func (f *decompressor) Read(b []byte) (int, error) {
if f.err != nil {
return 0, f.err
}
f.step(f)

f.doStep()

if f.err != nil && len(f.toRead) == 0 {
f.toRead = f.dict.readFlush() // Flush what's left in case of error
}
}
}

// Support the io.WriteTo interface for io.Copy and friends.
// WriteTo implements the io.WriteTo interface for io.Copy and friends.
func (f *decompressor) WriteTo(w io.Writer) (int64, error) {
total := int64(0)
flushed := false
Expand All @@ -410,7 +425,7 @@ func (f *decompressor) WriteTo(w io.Writer) (int64, error) {
return total, f.err
}
if f.err == nil {
f.step(f)
f.doStep()
}
if len(f.toRead) == 0 && f.err != nil && !flushed {
f.toRead = f.dict.readFlush() // Flush what's left in case of error
Expand Down Expand Up @@ -631,7 +646,7 @@ func (f *decompressor) copyData() {

if f.dict.availWrite() == 0 || f.copyLen > 0 {
f.toRead = f.dict.readFlush()
f.step = (*decompressor).copyData
f.step = copyData
return
}
f.finishBlock()
Expand All @@ -644,7 +659,28 @@ func (f *decompressor) finishBlock() {
}
f.err = io.EOF
}
f.step = (*decompressor).nextBlock
f.step = nextBlock
}

func (f *decompressor) doStep() {
switch f.step {
case copyData:
f.copyData()
case nextBlock:
f.nextBlock()
case huffmanBytesBuffer:
f.huffmanBytesBuffer()
case huffmanBytesReader:
f.huffmanBytesReader()
case huffmanBufioReader:
f.huffmanBufioReader()
case huffmanStringsReader:
f.huffmanStringsReader()
case huffmanGenericReader:
f.huffmanGenericReader()
default:
panic("BUG: unexpected step state")
}
}

// noEOF returns err, unless err == io.EOF, in which case it returns io.ErrUnexpectedEOF.
Expand Down Expand Up @@ -747,7 +783,7 @@ func (f *decompressor) Reset(r io.Reader, dict []byte) error {
h1: f.h1,
h2: f.h2,
dict: f.dict,
step: (*decompressor).nextBlock,
step: nextBlock,
}
f.dict.init(maxMatchOffset, dict)
return nil
Expand All @@ -768,7 +804,7 @@ func NewReader(r io.Reader) io.ReadCloser {
f.r = makeReader(r)
f.bits = new([maxNumLit + maxNumDist]int)
f.codebits = new([numCodes]int)
f.step = (*decompressor).nextBlock
f.step = nextBlock
f.dict.init(maxMatchOffset, nil)
return &f
}
Expand All @@ -787,7 +823,7 @@ func NewReaderDict(r io.Reader, dict []byte) io.ReadCloser {
f.r = makeReader(r)
f.bits = new([maxNumLit + maxNumDist]int)
f.codebits = new([numCodes]int)
f.step = (*decompressor).nextBlock
f.step = nextBlock
f.dict.init(maxMatchOffset, dict)
return &f
}
34 changes: 17 additions & 17 deletions flate/inflate_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 5 additions & 4 deletions flate/reader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,14 @@ func benchmarkDecode(b *testing.B, testfile, level, n int) {
w.Close()
buf1 := compressed.Bytes()
buf0, compressed, w = nil, nil, nil
runtime.GC()
b.StartTimer()
r := NewReader(bytes.NewReader(buf1))
res := r.(Resetter)
runtime.GC()
b.StartTimer()

for i := 0; i < b.N; i++ {
res.Reset(bytes.NewReader(buf1), nil)
io.Copy(io.Discard, r)
_ = res.Reset(bytes.NewReader(buf1), nil)
_, _ = io.Copy(io.Discard, r)
}
}

Expand Down

0 comments on commit c051ef1

Please sign in to comment.