Skip to content

Commit 154ca32

Browse files
authored
rlp: optimize byte array handling (#22924)
This change improves the performance of encoding/decoding [N]byte. name old time/op new time/op delta DecodeByteArrayStruct-8 336ns ± 0% 246ns ± 0% -26.98% (p=0.000 n=9+10) EncodeByteArrayStruct-8 225ns ± 1% 148ns ± 1% -34.12% (p=0.000 n=10+10) name old alloc/op new alloc/op delta DecodeByteArrayStruct-8 120B ± 0% 48B ± 0% -60.00% (p=0.000 n=10+10) EncodeByteArrayStruct-8 0.00B 0.00B ~ (all equal)
1 parent 0d076d9 commit 154ca32

File tree

6 files changed

+146
-50
lines changed

6 files changed

+146
-50
lines changed

rlp/decode.go

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -348,25 +348,23 @@ func decodeByteArray(s *Stream, val reflect.Value) error {
348348
if err != nil {
349349
return err
350350
}
351-
vlen := val.Len()
351+
slice := byteArrayBytes(val)
352352
switch kind {
353353
case Byte:
354-
if vlen == 0 {
354+
if len(slice) == 0 {
355355
return &decodeError{msg: "input string too long", typ: val.Type()}
356-
}
357-
if vlen > 1 {
356+
} else if len(slice) > 1 {
358357
return &decodeError{msg: "input string too short", typ: val.Type()}
359358
}
360-
bv, _ := s.Uint()
361-
val.Index(0).SetUint(bv)
359+
slice[0] = s.byteval
360+
s.kind = -1
362361
case String:
363-
if uint64(vlen) < size {
362+
if uint64(len(slice)) < size {
364363
return &decodeError{msg: "input string too long", typ: val.Type()}
365364
}
366-
if uint64(vlen) > size {
365+
if uint64(len(slice)) > size {
367366
return &decodeError{msg: "input string too short", typ: val.Type()}
368367
}
369-
slice := val.Slice(0, vlen).Interface().([]byte)
370368
if err := s.readFull(slice); err != nil {
371369
return err
372370
}

rlp/decode_test.go

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import (
2626
"reflect"
2727
"strings"
2828
"testing"
29+
30+
"github.com/ethereum/go-ethereum/common/math"
2931
)
3032

3133
func TestStreamKind(t *testing.T) {
@@ -1063,7 +1065,7 @@ func ExampleStream() {
10631065
// [102 111 111 98 97 114] <nil>
10641066
}
10651067

1066-
func BenchmarkDecode(b *testing.B) {
1068+
func BenchmarkDecodeUints(b *testing.B) {
10671069
enc := encodeTestSlice(90000)
10681070
b.SetBytes(int64(len(enc)))
10691071
b.ReportAllocs()
@@ -1078,7 +1080,7 @@ func BenchmarkDecode(b *testing.B) {
10781080
}
10791081
}
10801082

1081-
func BenchmarkDecodeIntSliceReuse(b *testing.B) {
1083+
func BenchmarkDecodeUintsReused(b *testing.B) {
10821084
enc := encodeTestSlice(100000)
10831085
b.SetBytes(int64(len(enc)))
10841086
b.ReportAllocs()
@@ -1093,6 +1095,44 @@ func BenchmarkDecodeIntSliceReuse(b *testing.B) {
10931095
}
10941096
}
10951097

1098+
func BenchmarkDecodeByteArrayStruct(b *testing.B) {
1099+
enc, err := EncodeToBytes(&byteArrayStruct{})
1100+
if err != nil {
1101+
b.Fatal(err)
1102+
}
1103+
b.SetBytes(int64(len(enc)))
1104+
b.ReportAllocs()
1105+
b.ResetTimer()
1106+
1107+
var out byteArrayStruct
1108+
for i := 0; i < b.N; i++ {
1109+
if err := DecodeBytes(enc, &out); err != nil {
1110+
b.Fatal(err)
1111+
}
1112+
}
1113+
}
1114+
1115+
func BenchmarkDecodeBigInts(b *testing.B) {
1116+
ints := make([]*big.Int, 200)
1117+
for i := range ints {
1118+
ints[i] = math.BigPow(2, int64(i))
1119+
}
1120+
enc, err := EncodeToBytes(ints)
1121+
if err != nil {
1122+
b.Fatal(err)
1123+
}
1124+
b.SetBytes(int64(len(enc)))
1125+
b.ReportAllocs()
1126+
b.ResetTimer()
1127+
1128+
var out []*big.Int
1129+
for i := 0; i < b.N; i++ {
1130+
if err := DecodeBytes(enc, &out); err != nil {
1131+
b.Fatal(err)
1132+
}
1133+
}
1134+
}
1135+
10961136
func encodeTestSlice(n uint) []byte {
10971137
s := make([]uint, n)
10981138
for i := uint(0); i < n; i++ {

rlp/encode.go

Lines changed: 17 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -124,19 +124,15 @@ func puthead(buf []byte, smalltag, largetag byte, size uint64) int {
124124
}
125125

126126
type encbuf struct {
127-
str []byte // string data, contains everything except list headers
128-
lheads []listhead // all list headers
129-
lhsize int // sum of sizes of all encoded list headers
130-
sizebuf [9]byte // auxiliary buffer for uint encoding
131-
bufvalue reflect.Value // used in writeByteArrayCopy
127+
str []byte // string data, contains everything except list headers
128+
lheads []listhead // all list headers
129+
lhsize int // sum of sizes of all encoded list headers
130+
sizebuf [9]byte // auxiliary buffer for uint encoding
132131
}
133132

134133
// encbufs are pooled.
135134
var encbufPool = sync.Pool{
136-
New: func() interface{} {
137-
var bytes []byte
138-
return &encbuf{bufvalue: reflect.ValueOf(&bytes).Elem()}
139-
},
135+
New: func() interface{} { return new(encbuf) },
140136
}
141137

142138
func (w *encbuf) reset() {
@@ -429,21 +425,14 @@ func writeBytes(val reflect.Value, w *encbuf) error {
429425
return nil
430426
}
431427

432-
var byteType = reflect.TypeOf(byte(0))
433-
434428
func makeByteArrayWriter(typ reflect.Type) writer {
435-
length := typ.Len()
436-
if length == 0 {
429+
switch typ.Len() {
430+
case 0:
437431
return writeLengthZeroByteArray
438-
} else if length == 1 {
432+
case 1:
439433
return writeLengthOneByteArray
440-
}
441-
if typ.Elem() != byteType {
442-
return writeNamedByteArray
443-
}
444-
return func(val reflect.Value, w *encbuf) error {
445-
writeByteArrayCopy(length, val, w)
446-
return nil
434+
default:
435+
return writeByteArray
447436
}
448437
}
449438

@@ -462,29 +451,18 @@ func writeLengthOneByteArray(val reflect.Value, w *encbuf) error {
462451
return nil
463452
}
464453

465-
// writeByteArrayCopy encodes byte arrays using reflect.Copy. This is
466-
// the fast path for [N]byte where N > 1.
467-
func writeByteArrayCopy(length int, val reflect.Value, w *encbuf) {
468-
w.encodeStringHeader(length)
469-
offset := len(w.str)
470-
w.str = append(w.str, make([]byte, length)...)
471-
w.bufvalue.SetBytes(w.str[offset:])
472-
reflect.Copy(w.bufvalue, val)
473-
}
474-
475-
// writeNamedByteArray encodes byte arrays with named element type.
476-
// This exists because reflect.Copy can't be used with such types.
477-
func writeNamedByteArray(val reflect.Value, w *encbuf) error {
454+
func writeByteArray(val reflect.Value, w *encbuf) error {
478455
if !val.CanAddr() {
479-
// Slice requires the value to be addressable.
480-
// Make it addressable by copying.
456+
// Getting the byte slice of val requires it to be addressable. Make it
457+
// addressable by copying.
481458
copy := reflect.New(val.Type()).Elem()
482459
copy.Set(val)
483460
val = copy
484461
}
485-
size := val.Len()
486-
slice := val.Slice(0, size).Bytes()
487-
w.encodeString(slice)
462+
463+
slice := byteArrayBytes(val)
464+
w.encodeStringHeader(len(slice))
465+
w.str = append(w.str, slice...)
488466
return nil
489467
}
490468

rlp/encode_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,3 +513,22 @@ func BenchmarkEncodeConcurrentInterface(b *testing.B) {
513513
}
514514
wg.Wait()
515515
}
516+
517+
type byteArrayStruct struct {
518+
A [20]byte
519+
B [32]byte
520+
C [32]byte
521+
}
522+
523+
func BenchmarkEncodeByteArrayStruct(b *testing.B) {
524+
var out bytes.Buffer
525+
var value byteArrayStruct
526+
527+
b.ReportAllocs()
528+
for i := 0; i < b.N; i++ {
529+
out.Reset()
530+
if err := Encode(&out, &value); err != nil {
531+
b.Fatal(err)
532+
}
533+
}
534+
}

rlp/safe.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright 2021 The go-ethereum Authors
2+
// This file is part of the go-ethereum library.
3+
//
4+
// The go-ethereum library is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Lesser General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// The go-ethereum library is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Lesser General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Lesser General Public License
15+
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
16+
17+
// +build nacl js !cgo
18+
19+
package rlp
20+
21+
import "reflect"
22+
23+
// byteArrayBytes returns a slice of the byte array v.
24+
func byteArrayBytes(v reflect.Value) []byte {
25+
return v.Slice(0, v.Len()).Bytes()
26+
}

rlp/unsafe.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright 2021 The go-ethereum Authors
2+
// This file is part of the go-ethereum library.
3+
//
4+
// The go-ethereum library is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Lesser General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// The go-ethereum library is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Lesser General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Lesser General Public License
15+
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
16+
17+
// +build !nacl,!js,cgo
18+
19+
package rlp
20+
21+
import (
22+
"reflect"
23+
"unsafe"
24+
)
25+
26+
// byteArrayBytes returns a slice of the byte array v.
27+
func byteArrayBytes(v reflect.Value) []byte {
28+
len := v.Len()
29+
var s []byte
30+
hdr := (*reflect.SliceHeader)(unsafe.Pointer(&s))
31+
hdr.Data = v.UnsafeAddr()
32+
hdr.Cap = len
33+
hdr.Len = len
34+
return s
35+
}

0 commit comments

Comments
 (0)