Skip to content

Commit ec0371e

Browse files
committed
codec: optimize decoding for ZeroCopy mode and use mode for benchmarks
NewDecoderBytes with ZeroCopy=true will return []byte and string that point into the input []byte. This reduces allocation and improves performance, in both codecgen and normal mode, showing ~ 50% reduction in allocation. To support this, we track decByteState after during each call to DecodeBytes or DecodeStringAsByte. Folks that care can check the value right after the call. We also optimized json decoding, so we can return a view into the input []byte if possible, instead of always doing a copy. Helper functions were added to make it easy to get a string value from a []byte with optimization e.g. zerocopy, interning, etc. We now run benchmarks by default with ZeroCopy=true.
1 parent 75391f7 commit ec0371e

24 files changed

+621
-438
lines changed

codec/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ some caveats. See Encode documentation.
246246

247247
```go
248248
const CborStreamBytes byte = 0x5f ...
249-
const GenVersion = 20
249+
const GenVersion = 21
250250
var SelfExt = &extFailWrapper{}
251251
var GoRpc goRpc
252252
var MsgpackSpecRpc msgpackSpecRpc

codec/bench/bench.sh

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ _usage() {
199199
printf "\t-sgx run test suite for codec; if g, use generated files; if x, do external also\n"
200200
printf "\t-jqp run test suite for [json, json-quick, json-profile]\n"
201201
printf "\t-z run tests for bench.out.txt\n"
202+
printf "\t-f [pprof file] run pprof\n"
202203
}
203204

204205
_main() {
@@ -211,10 +212,10 @@ _main() {
211212
local args=()
212213
local do_x="0"
213214
local do_g="0"
214-
while getopts "dcbsjqptxklgz" flag
215+
while getopts "dcbsjqptxklgzf" flag
215216
do
216217
case "$flag" in
217-
d|c|b|s|j|q|p|t|x|k|l|g|z) args+=( "$flag" ) ;;
218+
d|c|b|s|j|q|p|t|x|k|l|g|z|f) args+=( "$flag" ) ;;
218219
*) _usage; return 1 ;;
219220
esac
220221
done
@@ -237,6 +238,7 @@ _main() {
237238
[[ " ${args[*]} " == *"p"* ]] && _suite_very_quick_json_only_profile "$@" | _suite_trim_output
238239
[[ " ${args[*]} " == *"t"* ]] && _suite_tests "$@" | _suite_trim_output | _suite_tests_strip_file_line
239240
[[ " ${args[*]} " == *"z"* ]] && _bench_dot_out_dot_txt
241+
[[ " ${args[*]} " == *"f"* ]] && ${gocmd} tool pprof bench.test ${1:-mem.out}
240242

241243
true
242244
# shift $((OPTIND-1))

codec/bench/bench_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,13 @@ func benchPreInit() {
7878
// if bytesLen < approxSize {
7979
// bytesLen = approxSize
8080
// }
81+
82+
// use zerocopy for the benchmarks, for best performance
83+
testJsonH.ZeroCopy = true
84+
testCborH.ZeroCopy = true
85+
testMsgpackH.ZeroCopy = true
86+
testSimpleH.ZeroCopy = true
87+
testBincH.ZeroCopy = true
8188
}
8289

8390
func benchReinit() {

codec/bench/shared_test.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -170,9 +170,6 @@ func testHEDGet(h Handle) *testHED {
170170
func testReinit() {
171171
testOnce = sync.Once{}
172172
testHEDs = nil
173-
// for _, v := range testHandles {
174-
// v.getBasicHandle().clearInited() // so it is reinitialized next time around
175-
// }
176173
}
177174

178175
func testInitAll() {

codec/binc.go

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -702,24 +702,22 @@ func (d *bincDecDriver) decLenNumber() (v uint64) {
702702
return
703703
}
704704

705-
func (d *bincDecDriver) decStringBytes(bs []byte, zerocopy bool) (bs2 []byte) {
705+
// func (d *bincDecDriver) decStringBytes(bs []byte, zerocopy bool) (bs2 []byte) {
706+
func (d *bincDecDriver) DecodeStringAsBytes() (bs2 []byte) {
707+
d.d.decByteState = decByteStateNone
706708
if d.advanceNil() {
707709
return
708710
}
709711
var slen = -1
710712
switch d.vd {
711713
case bincVdString, bincVdByteArray:
712714
slen = d.decLen()
713-
if zerocopy {
714-
if d.d.bytes {
715-
bs2 = d.d.decRd.rb.readx(uint(slen))
716-
} else if len(bs) == 0 {
717-
bs2 = decByteSlice(d.d.r(), slen, d.d.h.MaxInitLen, d.d.b[:])
718-
} else {
719-
bs2 = decByteSlice(d.d.r(), slen, d.d.h.MaxInitLen, bs)
720-
}
715+
if d.d.bytes {
716+
d.d.decByteState = decByteStateZerocopy
717+
bs2 = d.d.decRd.rb.readx(uint(slen))
721718
} else {
722-
bs2 = decByteSlice(d.d.r(), slen, d.d.h.MaxInitLen, bs)
719+
d.d.decByteState = decByteStateReuseBuf
720+
bs2 = decByteSlice(d.d.r(), slen, d.d.h.MaxInitLen, d.d.b[:])
723721
}
724722
case bincVdSymbol:
725723
// zerocopy doesn't apply for symbols,
@@ -760,20 +758,21 @@ func (d *bincDecDriver) decStringBytes(bs []byte, zerocopy bool) (bs2 []byte) {
760758
return
761759
}
762760

763-
func (d *bincDecDriver) DecodeStringAsBytes() (s []byte) {
764-
return d.decStringBytes(d.d.b[:], true)
765-
}
766-
767-
func (d *bincDecDriver) DecodeBytes(bs []byte, zerocopy bool) (bsOut []byte) {
761+
func (d *bincDecDriver) DecodeBytes(bs []byte) (bsOut []byte) {
762+
d.d.decByteState = decByteStateNone
768763
if d.advanceNil() {
769764
return
770765
}
771766
if d.vd == bincVdArray {
772-
if zerocopy && len(bs) == 0 {
767+
if bs == nil {
773768
bs = d.d.b[:]
769+
d.d.decByteState = decByteStateReuseBuf
774770
}
775771
slen := d.ReadArrayStart()
776-
bs = usableByteSlice(bs, slen)
772+
var changed bool
773+
if bs, changed = usableByteSlice(bs, slen); changed {
774+
d.d.decByteState = decByteStateNone
775+
}
777776
for i := 0; i < slen; i++ {
778777
bs[i] = uint8(chkOvf.UintV(d.DecodeUint64(), 8))
779778
}
@@ -786,11 +785,13 @@ func (d *bincDecDriver) DecodeBytes(bs []byte, zerocopy bool) (bsOut []byte) {
786785
d.d.errorf("bytes - %s %x-%x/%s", msgBadDesc, d.vd, d.vs, bincdesc(d.vd, d.vs))
787786
}
788787
d.bdRead = false
789-
if d.d.bytes && (zerocopy || d.h.ZeroCopy) {
788+
if d.d.bytes && d.h.ZeroCopy {
789+
d.d.decByteState = decByteStateZerocopy
790790
return d.d.decRd.rb.readx(uint(clen))
791791
}
792-
if zerocopy && len(bs) == 0 {
792+
if bs == nil {
793793
bs = d.d.b[:]
794+
d.d.decByteState = decByteStateReuseBuf
794795
}
795796
return decByteSlice(d.d.r(), clen, d.d.h.MaxInitLen, bs)
796797
}
@@ -802,20 +803,20 @@ func (d *bincDecDriver) DecodeExt(rv interface{}, xtag uint64, ext Ext) {
802803
if d.advanceNil() {
803804
return
804805
}
805-
realxtag1, xbs := d.decodeExtV(ext != nil, uint8(xtag))
806+
xbs, realxtag1, zerocopy := d.decodeExtV(ext != nil, uint8(xtag))
806807
realxtag := uint64(realxtag1)
807808
if ext == nil {
808809
re := rv.(*RawExt)
809810
re.Tag = realxtag
810-
re.Data = detachZeroCopyBytes(d.d.bytes, re.Data, xbs)
811+
re.setData(xbs, zerocopy)
811812
} else if ext == SelfExt {
812813
d.d.sideDecode(rv, xbs)
813814
} else {
814815
ext.ReadExt(rv, xbs)
815816
}
816817
}
817818

818-
func (d *bincDecDriver) decodeExtV(verifyTag bool, tag byte) (xtag byte, xbs []byte) {
819+
func (d *bincDecDriver) decodeExtV(verifyTag bool, tag byte) (xbs []byte, xtag byte, zerocopy bool) {
819820
if d.vd == bincVdCustomExt {
820821
l := d.decLen()
821822
xtag = d.d.decRd.readn1()
@@ -824,11 +825,12 @@ func (d *bincDecDriver) decodeExtV(verifyTag bool, tag byte) (xtag byte, xbs []b
824825
}
825826
if d.d.bytes {
826827
xbs = d.d.decRd.rb.readx(uint(l))
828+
zerocopy = true
827829
} else {
828830
xbs = decByteSlice(d.d.r(), l, d.d.h.MaxInitLen, d.d.b[:])
829831
}
830832
} else if d.vd == bincVdByteArray {
831-
xbs = d.DecodeBytes(nil, true)
833+
xbs = d.DecodeBytes(nil)
832834
} else {
833835
d.d.errorf("ext expects extensions or byte array - %s %x-%x/%s", msgBadDesc, d.vd, d.vs, bincdesc(d.vd, d.vs))
834836
}
@@ -890,12 +892,12 @@ func (d *bincDecDriver) DecodeNaked() {
890892
n.f = d.decFloat()
891893
case bincVdString:
892894
n.v = valueTypeString
893-
n.s = d.d.string(d.DecodeStringAsBytes())
895+
n.s = d.d.stringZC(d.DecodeStringAsBytes())
894896
case bincVdByteArray:
895-
d.d.fauxUnionReadRawBytes()
897+
d.d.fauxUnionReadRawBytes(false)
896898
case bincVdSymbol:
897899
n.v = valueTypeSymbol
898-
n.s = d.d.string(d.DecodeStringAsBytes())
900+
n.s = d.d.stringZC(d.DecodeStringAsBytes())
899901
case bincVdTimestamp:
900902
n.v = valueTypeTime
901903
tt, err := bincDecodeTime(d.d.decRd.readx(uint(d.vs)))

codec/cbor.go

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -572,7 +572,8 @@ func (d *cborDecDriver) ReadArrayStart() (length int) {
572572
return d.decLen()
573573
}
574574

575-
func (d *cborDecDriver) DecodeBytes(bs []byte, zerocopy bool) (bsOut []byte) {
575+
func (d *cborDecDriver) DecodeBytes(bs []byte) (bsOut []byte) {
576+
d.d.decByteState = decByteStateNone
576577
if d.advanceNil() {
577578
return
578579
}
@@ -582,20 +583,16 @@ func (d *cborDecDriver) DecodeBytes(bs []byte, zerocopy bool) (bsOut []byte) {
582583
if d.bd == cborBdIndefiniteBytes || d.bd == cborBdIndefiniteString {
583584
d.bdRead = false
584585
if bs == nil {
585-
if zerocopy {
586-
return d.decAppendIndefiniteBytes(d.d.b[:0])
587-
}
588-
return d.decAppendIndefiniteBytes(zeroByteSlice)
586+
d.d.decByteState = decByteStateReuseBuf
587+
return d.decAppendIndefiniteBytes(d.d.b[:0])
589588
}
590589
return d.decAppendIndefiniteBytes(bs[:0])
591590
}
592591
if d.bd == cborBdIndefiniteArray {
593592
d.bdRead = false
594-
if zerocopy && len(bs) == 0 {
595-
bs = d.d.b[:]
596-
}
597593
if bs == nil {
598-
bs = []byte{}
594+
d.d.decByteState = decByteStateReuseBuf
595+
bs = d.d.b[:0]
599596
} else {
600597
bs = bs[:0]
601598
}
@@ -606,29 +603,35 @@ func (d *cborDecDriver) DecodeBytes(bs []byte, zerocopy bool) (bsOut []byte) {
606603
}
607604
if d.bd>>5 == cborMajorArray {
608605
d.bdRead = false
609-
if zerocopy && len(bs) == 0 {
606+
if bs == nil {
607+
d.d.decByteState = decByteStateReuseBuf
610608
bs = d.d.b[:]
611609
}
612610
slen := d.decLen()
613-
bs = usableByteSlice(bs, slen)
611+
var changed bool
612+
if bs, changed = usableByteSlice(bs, slen); changed {
613+
d.d.decByteState = decByteStateNone
614+
}
614615
for i := 0; i < len(bs); i++ {
615616
bs[i] = uint8(chkOvf.UintV(d.DecodeUint64(), 8))
616617
}
617618
return bs
618619
}
619620
clen := d.decLen()
620621
d.bdRead = false
621-
if d.d.bytes && (zerocopy || d.h.ZeroCopy) {
622+
if d.d.bytes && d.h.ZeroCopy {
623+
d.d.decByteState = decByteStateZerocopy
622624
return d.d.decRd.rb.readx(uint(clen))
623625
}
624-
if zerocopy && len(bs) == 0 {
626+
if bs == nil {
627+
d.d.decByteState = decByteStateReuseBuf
625628
bs = d.d.b[:]
626629
}
627630
return decByteSlice(d.d.r(), clen, d.h.MaxInitLen, bs)
628631
}
629632

630633
func (d *cborDecDriver) DecodeStringAsBytes() (s []byte) {
631-
return d.DecodeBytes(d.d.b[:], true)
634+
return d.DecodeBytes(nil)
632635
}
633636

634637
func (d *cborDecDriver) DecodeTime() (t time.Time) {
@@ -704,10 +707,10 @@ func (d *cborDecDriver) DecodeNaked() {
704707
n.v = valueTypeInt
705708
n.i = d.DecodeInt64()
706709
case cborMajorBytes:
707-
d.d.fauxUnionReadRawBytes()
710+
d.d.fauxUnionReadRawBytes(false)
708711
case cborMajorString:
709712
n.v = valueTypeString
710-
n.s = d.d.string(d.DecodeStringAsBytes())
713+
n.s = d.d.stringZC(d.DecodeStringAsBytes())
711714
case cborMajorArray:
712715
n.v = valueTypeArray
713716
decodeFurther = true

codec/codec_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1530,7 +1530,7 @@ func doTestMapEncodeForCanonical(t *testing.T, h Handle) {
15301530
if !bytes.Equal(b1t, b2t) {
15311531
t.Logf("Unequal bytes of length: %v vs %v", len(b1t), len(b2t))
15321532
if testVerbose {
1533-
t.Logf("Unequal bytes: %v VS %v", b1t, b2t)
1533+
t.Logf("Unequal bytes: \n\t%v \n\tVS \n\t%v", b1t, b2t)
15341534
}
15351535
t.FailNow()
15361536
}

0 commit comments

Comments
 (0)