Skip to content

Commit 4a2d40e

Browse files
authored
zstd: Reduce per decoder allocations significantly (#252)
* zstd: Reduce decoder allocations significantly * More strict window size check.
1 parent cc0d76e commit 4a2d40e

File tree

3 files changed

+84
-7
lines changed

3 files changed

+84
-7
lines changed

zstd/blockdec.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,17 +131,25 @@ func (b *blockDec) reset(br byteBuffer, windowSize uint64) error {
131131
b.Type = blockType((bh >> 1) & 3)
132132
// find size.
133133
cSize := int(bh >> 3)
134+
maxSize := maxBlockSize
134135
switch b.Type {
135136
case blockTypeReserved:
136137
return ErrReservedBlockType
137138
case blockTypeRLE:
138139
b.RLESize = uint32(cSize)
140+
if b.lowMem {
141+
maxSize = cSize
142+
}
139143
cSize = 1
140144
case blockTypeCompressed:
141145
if debug {
142146
println("Data size on stream:", cSize)
143147
}
144148
b.RLESize = 0
149+
maxSize = maxCompressedBlockSize
150+
if windowSize < maxCompressedBlockSize && b.lowMem {
151+
maxSize = int(windowSize)
152+
}
145153
if cSize > maxCompressedBlockSize || uint64(cSize) > b.WindowSize {
146154
if debug {
147155
printf("compressed block too big: csize:%d block: %+v\n", uint64(cSize), b)
@@ -160,8 +168,8 @@ func (b *blockDec) reset(br byteBuffer, windowSize uint64) error {
160168
b.dataStorage = make([]byte, 0, maxBlockSize)
161169
}
162170
}
163-
if cap(b.dst) <= maxBlockSize {
164-
b.dst = make([]byte, 0, maxBlockSize+1)
171+
if cap(b.dst) <= maxSize {
172+
b.dst = make([]byte, 0, maxSize+1)
165173
}
166174
var err error
167175
b.data, err = br.readBig(cSize, b.dataStorage)
@@ -679,8 +687,11 @@ func (b *blockDec) decodeCompressed(hist *history) error {
679687
println("initializing sequences:", err)
680688
return err
681689
}
682-
683-
err = seqs.decode(nSeqs, br, hist.b)
690+
hbytes := hist.b
691+
if len(hbytes) > hist.windowSize {
692+
hbytes = hbytes[len(hbytes)-hist.windowSize:]
693+
}
694+
err = seqs.decode(nSeqs, br, hbytes)
684695
if err != nil {
685696
return err
686697
}

zstd/decoder_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,68 @@ func TestNewDecoder(t *testing.T) {
149149
testDecoderDecodeAll(t, "testdata/decoder.zip", dec)
150150
}
151151

152+
func TestNewDecoderMemory(t *testing.T) {
153+
defer timeout(60 * time.Second)()
154+
var testdata bytes.Buffer
155+
enc, err := NewWriter(&testdata, WithWindowSize(64<<10), WithSingleSegment(false))
156+
if err != nil {
157+
t.Fatal(err)
158+
}
159+
// Write 256KB
160+
for i := 0; i < 256; i++ {
161+
tmp := strings.Repeat(string([]byte{byte(i)}), 1024)
162+
_, err := enc.Write([]byte(tmp))
163+
if err != nil {
164+
t.Fatal(err)
165+
}
166+
}
167+
err = enc.Close()
168+
if err != nil {
169+
t.Fatal(err)
170+
}
171+
172+
var n = 5000
173+
if testing.Short() {
174+
n = 200
175+
}
176+
177+
var before, after runtime.MemStats
178+
runtime.GC()
179+
runtime.ReadMemStats(&before)
180+
181+
var decs = make([]*Decoder, n)
182+
for i := range decs {
183+
// Wrap in NopCloser to avoid shortcut.
184+
input := ioutil.NopCloser(bytes.NewBuffer(testdata.Bytes()))
185+
decs[i], err = NewReader(input, WithDecoderConcurrency(1), WithDecoderLowmem(true))
186+
if err != nil {
187+
t.Fatal(err)
188+
}
189+
}
190+
191+
// 32K buffer
192+
var tmp [128 << 10]byte
193+
for i := range decs {
194+
_, err := io.ReadFull(decs[i], tmp[:])
195+
if err != nil {
196+
t.Fatal(err)
197+
}
198+
}
199+
200+
runtime.GC()
201+
runtime.ReadMemStats(&after)
202+
size := (after.HeapInuse - before.HeapInuse) / uint64(n) / 1024
203+
t.Log(size, "KiB per decoder")
204+
// This is not exact science, but fail if we suddenly get more than 2x what we expect.
205+
if size > 221*2 && !testing.Short() {
206+
t.Errorf("expected < 221KB per decoder, got %d", size)
207+
}
208+
209+
for _, dec := range decs {
210+
dec.Close()
211+
}
212+
}
213+
152214
func TestNewDecoderGood(t *testing.T) {
153215
defer timeout(30 * time.Second)()
154216
testDecoderFile(t, "testdata/good.zip")

zstd/framedec.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,11 @@ func (d *frameDec) reset(br byteBuffer) error {
233233
return ErrWindowSizeTooSmall
234234
}
235235
d.history.windowSize = int(d.WindowSize)
236-
d.history.maxSize = d.history.windowSize + maxBlockSize
236+
if d.o.lowMem && d.history.windowSize < maxBlockSize {
237+
d.history.maxSize = d.history.windowSize * 2
238+
} else {
239+
d.history.maxSize = d.history.windowSize + maxBlockSize
240+
}
237241
// history contains input - maybe we do something
238242
d.rawInput = br
239243
return nil
@@ -320,8 +324,8 @@ func (d *frameDec) checkCRC() error {
320324

321325
func (d *frameDec) initAsync() {
322326
if !d.o.lowMem && !d.SingleSegment {
323-
// set max extra size history to 20MB.
324-
d.history.maxSize = d.history.windowSize + maxBlockSize*10
327+
// set max extra size history to 10MB.
328+
d.history.maxSize = d.history.windowSize + maxBlockSize*5
325329
}
326330
// re-alloc if more than one extra block size.
327331
if d.o.lowMem && cap(d.history.b) > d.history.maxSize+maxBlockSize {

0 commit comments

Comments
 (0)