Skip to content

Commit c9d89f6

Browse files
lmbbradfitz
authored andcommitted
encoding/binary: cache struct sizes to speed up Read and Write
A majority of work is spent in dataSize when en/decoding the same struct over and over again. This wastes a lot of work, since the result doesn't change for a given reflect.Value. Cache the result of the function for structs, so that subsequent calls to dataSize can avoid doing work. name old time/op new time/op delta ReadStruct 1.00µs ± 1% 0.37µs ± 1% -62.99% (p=0.029 n=4+4) WriteStruct 1.00µs ± 3% 0.37µs ± 1% -62.69% (p=0.008 n=5+5) name old speed new speed delta ReadStruct 75.1MB/s ± 1% 202.9MB/s ± 1% +170.16% (p=0.029 n=4+4) WriteStruct 74.8MB/s ± 3% 200.4MB/s ± 1% +167.96% (p=0.008 n=5+5) Fixes #34471 Change-Id: Ic5d987ca95f1197415ef93643a0af6fc1224fdf0 Reviewed-on: https://go-review.googlesource.com/c/go/+/199539 Reviewed-by: Brad Fitzpatrick <[email protected]> Run-TryBot: Brad Fitzpatrick <[email protected]> TryBot-Result: Gobot Gobot <[email protected]>
1 parent 2c8529c commit c9d89f6

File tree

2 files changed

+79
-2
lines changed

2 files changed

+79
-2
lines changed

src/encoding/binary/binary.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"io"
2727
"math"
2828
"reflect"
29+
"sync"
2930
)
3031

3132
// A ByteOrder specifies how to convert byte sequences into
@@ -363,18 +364,32 @@ func Size(v interface{}) int {
363364
return dataSize(reflect.Indirect(reflect.ValueOf(v)))
364365
}
365366

367+
var structSize sync.Map // map[reflect.Type]int
368+
366369
// dataSize returns the number of bytes the actual data represented by v occupies in memory.
367370
// For compound structures, it sums the sizes of the elements. Thus, for instance, for a slice
368371
// it returns the length of the slice times the element size and does not count the memory
369372
// occupied by the header. If the type of v is not acceptable, dataSize returns -1.
370373
func dataSize(v reflect.Value) int {
371-
if v.Kind() == reflect.Slice {
374+
switch v.Kind() {
375+
case reflect.Slice:
372376
if s := sizeof(v.Type().Elem()); s >= 0 {
373377
return s * v.Len()
374378
}
375379
return -1
380+
381+
case reflect.Struct:
382+
t := v.Type()
383+
if size, ok := structSize.Load(t); ok {
384+
return size.(int)
385+
}
386+
size := sizeof(t)
387+
structSize.Store(t, size)
388+
return size
389+
390+
default:
391+
return sizeof(v.Type())
376392
}
377-
return sizeof(v.Type())
378393
}
379394

380395
// sizeof returns the size >= 0 of variables for the given type or -1 if the type is not acceptable.

src/encoding/binary/binary_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ package binary
77
import (
88
"bytes"
99
"io"
10+
"io/ioutil"
1011
"math"
1112
"reflect"
1213
"strings"
14+
"sync"
1315
"testing"
1416
)
1517

@@ -296,6 +298,58 @@ func TestBlankFields(t *testing.T) {
296298
}
297299
}
298300

301+
func TestSizeStructCache(t *testing.T) {
302+
// Reset the cache, otherwise multiple test runs fail.
303+
structSize = sync.Map{}
304+
305+
count := func() int {
306+
var i int
307+
structSize.Range(func(_, _ interface{}) bool {
308+
i++
309+
return true
310+
})
311+
return i
312+
}
313+
314+
var total int
315+
added := func() int {
316+
delta := count() - total
317+
total += delta
318+
return delta
319+
}
320+
321+
type foo struct {
322+
A uint32
323+
}
324+
325+
type bar struct {
326+
A Struct
327+
B foo
328+
C Struct
329+
}
330+
331+
testcases := []struct {
332+
val interface{}
333+
want int
334+
}{
335+
{new(foo), 1},
336+
{new(bar), 1},
337+
{new(bar), 0},
338+
{new(struct{ A Struct }), 1},
339+
{new(struct{ A Struct }), 0},
340+
}
341+
342+
for _, tc := range testcases {
343+
if Size(tc.val) == -1 {
344+
t.Fatalf("Can't get the size of %T", tc.val)
345+
}
346+
347+
if n := added(); n != tc.want {
348+
t.Errorf("Sizing %T added %d entries to the cache, want %d", tc.val, n, tc.want)
349+
}
350+
}
351+
}
352+
299353
// An attempt to read into a struct with an unexported field will
300354
// panic. This is probably not the best choice, but at this point
301355
// anything else would be an API change.
@@ -436,6 +490,14 @@ func BenchmarkReadStruct(b *testing.B) {
436490
}
437491
}
438492

493+
func BenchmarkWriteStruct(b *testing.B) {
494+
b.SetBytes(int64(Size(&s)))
495+
b.ResetTimer()
496+
for i := 0; i < b.N; i++ {
497+
Write(ioutil.Discard, BigEndian, &s)
498+
}
499+
}
500+
439501
func BenchmarkReadInts(b *testing.B) {
440502
var ls Struct
441503
bsr := &byteSliceReader{}

0 commit comments

Comments
 (0)