Skip to content

Commit 93a18ac

Browse files
cezarsanigeltao
authored andcommitted
image/png: reduce memory allocs encoding images by reusing buffers
This change allows greatly reducing memory allocations with a slightly performance improvement as well. Instances of (*png).Encoder can have a optional BufferPool attached to them. This allows reusing temporary buffers used when encoding a new image. This buffers include instances to zlib.Writer and bufio.Writer. Also, buffers for current and previous rows are saved in the encoder instance and reused as long as their cap() is enough to fit the current image row. A new benchmark was added to demonstrate the performance improvement when setting a BufferPool to an Encoder instance: $ go test -bench BenchmarkEncodeGray -benchmem BenchmarkEncodeGray-4 1000 2349584 ns/op 130.75 MB/s 852230 B/op 32 allocs/op BenchmarkEncodeGrayWithBufferPool-4 1000 2241650 ns/op 137.04 MB/s 900 B/op 3 allocs/op Change-Id: I4488201ae53cb2ad010c68c1e0118ee12beae14e Reviewed-on: https://go-review.googlesource.com/34150 Reviewed-by: Nigel Tao <[email protected]> Run-TryBot: Nigel Tao <[email protected]> TryBot-Result: Gobot Gobot <[email protected]>
1 parent 5030bfd commit 93a18ac

File tree

2 files changed

+107
-24
lines changed

2 files changed

+107
-24
lines changed

src/image/png/writer.go

Lines changed: 82 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,37 @@ import (
1717
// Encoder configures encoding PNG images.
1818
type Encoder struct {
1919
CompressionLevel CompressionLevel
20+
21+
// BufferPool optionally specifies a buffer pool to get temporary
22+
// EncoderBuffers when encoding an image.
23+
BufferPool EncoderBufferPool
24+
}
25+
26+
// EncoderBufferPool is an interface for getting and returning temporary
27+
// instances of the EncoderBuffer struct. This can be used to reuse buffers
28+
// when encoding multiple images.
29+
type EncoderBufferPool interface {
30+
Get() *EncoderBuffer
31+
Put(*EncoderBuffer)
2032
}
2133

34+
// EncoderBuffer holds the buffers used for encoding PNG images.
35+
type EncoderBuffer encoder
36+
2237
type encoder struct {
23-
enc *Encoder
24-
w io.Writer
25-
m image.Image
26-
cb int
27-
err error
28-
header [8]byte
29-
footer [4]byte
30-
tmp [4 * 256]byte
38+
enc *Encoder
39+
w io.Writer
40+
m image.Image
41+
cb int
42+
err error
43+
header [8]byte
44+
footer [4]byte
45+
tmp [4 * 256]byte
46+
cr [nFilter][]uint8
47+
pr []uint8
48+
zw *zlib.Writer
49+
zwLevel int
50+
bw *bufio.Writer
3151
}
3252

3353
type CompressionLevel int
@@ -273,12 +293,24 @@ func filter(cr *[nFilter][]byte, pr []byte, bpp int) int {
273293
return filter
274294
}
275295

276-
func writeImage(w io.Writer, m image.Image, cb int, level int) error {
277-
zw, err := zlib.NewWriterLevel(w, level)
278-
if err != nil {
279-
return err
296+
func zeroMemory(v []uint8) {
297+
for i := range v {
298+
v[i] = 0
299+
}
300+
}
301+
302+
func (e *encoder) writeImage(w io.Writer, m image.Image, cb int, level int) error {
303+
if e.zw == nil || e.zwLevel != level {
304+
zw, err := zlib.NewWriterLevel(w, level)
305+
if err != nil {
306+
return err
307+
}
308+
e.zw = zw
309+
e.zwLevel = level
310+
} else {
311+
e.zw.Reset(w)
280312
}
281-
defer zw.Close()
313+
defer e.zw.Close()
282314

283315
bpp := 0 // Bytes per pixel.
284316

@@ -304,12 +336,23 @@ func writeImage(w io.Writer, m image.Image, cb int, level int) error {
304336
// other PNG filter types. These buffers are allocated once and re-used for each row.
305337
// The +1 is for the per-row filter type, which is at cr[*][0].
306338
b := m.Bounds()
307-
var cr [nFilter][]uint8
308-
for i := range cr {
309-
cr[i] = make([]uint8, 1+bpp*b.Dx())
310-
cr[i][0] = uint8(i)
339+
sz := 1 + bpp*b.Dx()
340+
for i := range e.cr {
341+
if cap(e.cr[i]) < sz {
342+
e.cr[i] = make([]uint8, sz)
343+
} else {
344+
e.cr[i] = e.cr[i][:sz]
345+
}
346+
e.cr[i][0] = uint8(i)
347+
}
348+
cr := e.cr
349+
if cap(e.pr) < sz {
350+
e.pr = make([]uint8, sz)
351+
} else {
352+
e.pr = e.pr[:sz]
353+
zeroMemory(e.pr)
311354
}
312-
pr := make([]uint8, 1+bpp*b.Dx())
355+
pr := e.pr
313356

314357
gray, _ := m.(*image.Gray)
315358
rgba, _ := m.(*image.RGBA)
@@ -429,7 +472,7 @@ func writeImage(w io.Writer, m image.Image, cb int, level int) error {
429472
}
430473

431474
// Write the compressed bytes.
432-
if _, err := zw.Write(cr[f]); err != nil {
475+
if _, err := e.zw.Write(cr[f]); err != nil {
433476
return err
434477
}
435478

@@ -444,13 +487,16 @@ func (e *encoder) writeIDATs() {
444487
if e.err != nil {
445488
return
446489
}
447-
var bw *bufio.Writer
448-
bw = bufio.NewWriterSize(e, 1<<15)
449-
e.err = writeImage(bw, e.m, e.cb, levelToZlib(e.enc.CompressionLevel))
490+
if e.bw == nil {
491+
e.bw = bufio.NewWriterSize(e, 1<<15)
492+
} else {
493+
e.bw.Reset(e)
494+
}
495+
e.err = e.writeImage(e.bw, e.m, e.cb, levelToZlib(e.enc.CompressionLevel))
450496
if e.err != nil {
451497
return
452498
}
453-
e.err = bw.Flush()
499+
e.err = e.bw.Flush()
454500
}
455501

456502
// This function is required because we want the zero value of
@@ -489,7 +535,19 @@ func (enc *Encoder) Encode(w io.Writer, m image.Image) error {
489535
return FormatError("invalid image size: " + strconv.FormatInt(mw, 10) + "x" + strconv.FormatInt(mh, 10))
490536
}
491537

492-
var e encoder
538+
var e *encoder
539+
if enc.BufferPool != nil {
540+
buffer := enc.BufferPool.Get()
541+
e = (*encoder)(buffer)
542+
543+
}
544+
if e == nil {
545+
e = &encoder{}
546+
}
547+
if enc.BufferPool != nil {
548+
defer enc.BufferPool.Put((*EncoderBuffer)(e))
549+
}
550+
493551
e.enc = enc
494552
e.w = w
495553
e.m = m

src/image/png/writer_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,31 @@ func BenchmarkEncodeGray(b *testing.B) {
130130
}
131131
}
132132

133+
type pool struct {
134+
b *EncoderBuffer
135+
}
136+
137+
func (p *pool) Get() *EncoderBuffer {
138+
return p.b
139+
}
140+
141+
func (p *pool) Put(b *EncoderBuffer) {
142+
p.b = b
143+
}
144+
145+
func BenchmarkEncodeGrayWithBufferPool(b *testing.B) {
146+
b.StopTimer()
147+
img := image.NewGray(image.Rect(0, 0, 640, 480))
148+
e := Encoder{
149+
BufferPool: &pool{},
150+
}
151+
b.SetBytes(640 * 480 * 1)
152+
b.StartTimer()
153+
for i := 0; i < b.N; i++ {
154+
e.Encode(ioutil.Discard, img)
155+
}
156+
}
157+
133158
func BenchmarkEncodeNRGBOpaque(b *testing.B) {
134159
b.StopTimer()
135160
img := image.NewNRGBA(image.Rect(0, 0, 640, 480))

0 commit comments

Comments
 (0)