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)
This commit is contained in:
Felix Lange 2021-05-22 15:10:16 +02:00 committed by GitHub
parent 0d076d92db
commit 154ca32a8a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 146 additions and 50 deletions

@ -348,25 +348,23 @@ func decodeByteArray(s *Stream, val reflect.Value) error {
if err != nil { if err != nil {
return err return err
} }
vlen := val.Len() slice := byteArrayBytes(val)
switch kind { switch kind {
case Byte: case Byte:
if vlen == 0 { if len(slice) == 0 {
return &decodeError{msg: "input string too long", typ: val.Type()} return &decodeError{msg: "input string too long", typ: val.Type()}
} } else if len(slice) > 1 {
if vlen > 1 {
return &decodeError{msg: "input string too short", typ: val.Type()} return &decodeError{msg: "input string too short", typ: val.Type()}
} }
bv, _ := s.Uint() slice[0] = s.byteval
val.Index(0).SetUint(bv) s.kind = -1
case String: case String:
if uint64(vlen) < size { if uint64(len(slice)) < size {
return &decodeError{msg: "input string too long", typ: val.Type()} return &decodeError{msg: "input string too long", typ: val.Type()}
} }
if uint64(vlen) > size { if uint64(len(slice)) > size {
return &decodeError{msg: "input string too short", typ: val.Type()} return &decodeError{msg: "input string too short", typ: val.Type()}
} }
slice := val.Slice(0, vlen).Interface().([]byte)
if err := s.readFull(slice); err != nil { if err := s.readFull(slice); err != nil {
return err return err
} }

@ -26,6 +26,8 @@ import (
"reflect" "reflect"
"strings" "strings"
"testing" "testing"
"github.com/ethereum/go-ethereum/common/math"
) )
func TestStreamKind(t *testing.T) { func TestStreamKind(t *testing.T) {
@ -1063,7 +1065,7 @@ func ExampleStream() {
// [102 111 111 98 97 114] <nil> // [102 111 111 98 97 114] <nil>
} }
func BenchmarkDecode(b *testing.B) { func BenchmarkDecodeUints(b *testing.B) {
enc := encodeTestSlice(90000) enc := encodeTestSlice(90000)
b.SetBytes(int64(len(enc))) b.SetBytes(int64(len(enc)))
b.ReportAllocs() b.ReportAllocs()
@ -1078,7 +1080,7 @@ func BenchmarkDecode(b *testing.B) {
} }
} }
func BenchmarkDecodeIntSliceReuse(b *testing.B) { func BenchmarkDecodeUintsReused(b *testing.B) {
enc := encodeTestSlice(100000) enc := encodeTestSlice(100000)
b.SetBytes(int64(len(enc))) b.SetBytes(int64(len(enc)))
b.ReportAllocs() b.ReportAllocs()
@ -1093,6 +1095,44 @@ func BenchmarkDecodeIntSliceReuse(b *testing.B) {
} }
} }
func BenchmarkDecodeByteArrayStruct(b *testing.B) {
enc, err := EncodeToBytes(&byteArrayStruct{})
if err != nil {
b.Fatal(err)
}
b.SetBytes(int64(len(enc)))
b.ReportAllocs()
b.ResetTimer()
var out byteArrayStruct
for i := 0; i < b.N; i++ {
if err := DecodeBytes(enc, &out); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkDecodeBigInts(b *testing.B) {
ints := make([]*big.Int, 200)
for i := range ints {
ints[i] = math.BigPow(2, int64(i))
}
enc, err := EncodeToBytes(ints)
if err != nil {
b.Fatal(err)
}
b.SetBytes(int64(len(enc)))
b.ReportAllocs()
b.ResetTimer()
var out []*big.Int
for i := 0; i < b.N; i++ {
if err := DecodeBytes(enc, &out); err != nil {
b.Fatal(err)
}
}
}
func encodeTestSlice(n uint) []byte { func encodeTestSlice(n uint) []byte {
s := make([]uint, n) s := make([]uint, n)
for i := uint(0); i < n; i++ { for i := uint(0); i < n; i++ {

@ -124,19 +124,15 @@ func puthead(buf []byte, smalltag, largetag byte, size uint64) int {
} }
type encbuf struct { type encbuf struct {
str []byte // string data, contains everything except list headers str []byte // string data, contains everything except list headers
lheads []listhead // all list headers lheads []listhead // all list headers
lhsize int // sum of sizes of all encoded list headers lhsize int // sum of sizes of all encoded list headers
sizebuf [9]byte // auxiliary buffer for uint encoding sizebuf [9]byte // auxiliary buffer for uint encoding
bufvalue reflect.Value // used in writeByteArrayCopy
} }
// encbufs are pooled. // encbufs are pooled.
var encbufPool = sync.Pool{ var encbufPool = sync.Pool{
New: func() interface{} { New: func() interface{} { return new(encbuf) },
var bytes []byte
return &encbuf{bufvalue: reflect.ValueOf(&bytes).Elem()}
},
} }
func (w *encbuf) reset() { func (w *encbuf) reset() {
@ -429,21 +425,14 @@ func writeBytes(val reflect.Value, w *encbuf) error {
return nil return nil
} }
var byteType = reflect.TypeOf(byte(0))
func makeByteArrayWriter(typ reflect.Type) writer { func makeByteArrayWriter(typ reflect.Type) writer {
length := typ.Len() switch typ.Len() {
if length == 0 { case 0:
return writeLengthZeroByteArray return writeLengthZeroByteArray
} else if length == 1 { case 1:
return writeLengthOneByteArray return writeLengthOneByteArray
} default:
if typ.Elem() != byteType { return writeByteArray
return writeNamedByteArray
}
return func(val reflect.Value, w *encbuf) error {
writeByteArrayCopy(length, val, w)
return nil
} }
} }
@ -462,29 +451,18 @@ func writeLengthOneByteArray(val reflect.Value, w *encbuf) error {
return nil return nil
} }
// writeByteArrayCopy encodes byte arrays using reflect.Copy. This is func writeByteArray(val reflect.Value, w *encbuf) error {
// the fast path for [N]byte where N > 1.
func writeByteArrayCopy(length int, val reflect.Value, w *encbuf) {
w.encodeStringHeader(length)
offset := len(w.str)
w.str = append(w.str, make([]byte, length)...)
w.bufvalue.SetBytes(w.str[offset:])
reflect.Copy(w.bufvalue, val)
}
// writeNamedByteArray encodes byte arrays with named element type.
// This exists because reflect.Copy can't be used with such types.
func writeNamedByteArray(val reflect.Value, w *encbuf) error {
if !val.CanAddr() { if !val.CanAddr() {
// Slice requires the value to be addressable. // Getting the byte slice of val requires it to be addressable. Make it
// Make it addressable by copying. // addressable by copying.
copy := reflect.New(val.Type()).Elem() copy := reflect.New(val.Type()).Elem()
copy.Set(val) copy.Set(val)
val = copy val = copy
} }
size := val.Len()
slice := val.Slice(0, size).Bytes() slice := byteArrayBytes(val)
w.encodeString(slice) w.encodeStringHeader(len(slice))
w.str = append(w.str, slice...)
return nil return nil
} }

@ -513,3 +513,22 @@ func BenchmarkEncodeConcurrentInterface(b *testing.B) {
} }
wg.Wait() wg.Wait()
} }
type byteArrayStruct struct {
A [20]byte
B [32]byte
C [32]byte
}
func BenchmarkEncodeByteArrayStruct(b *testing.B) {
var out bytes.Buffer
var value byteArrayStruct
b.ReportAllocs()
for i := 0; i < b.N; i++ {
out.Reset()
if err := Encode(&out, &value); err != nil {
b.Fatal(err)
}
}
}

26
rlp/safe.go Normal file

@ -0,0 +1,26 @@
// Copyright 2021 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
// +build nacl js !cgo
package rlp
import "reflect"
// byteArrayBytes returns a slice of the byte array v.
func byteArrayBytes(v reflect.Value) []byte {
return v.Slice(0, v.Len()).Bytes()
}

35
rlp/unsafe.go Normal file

@ -0,0 +1,35 @@
// Copyright 2021 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
// +build !nacl,!js,cgo
package rlp
import (
"reflect"
"unsafe"
)
// byteArrayBytes returns a slice of the byte array v.
func byteArrayBytes(v reflect.Value) []byte {
len := v.Len()
var s []byte
hdr := (*reflect.SliceHeader)(unsafe.Pointer(&s))
hdr.Data = v.UnsafeAddr()
hdr.Cap = len
hdr.Len = len
return s
}