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 {
return err
}
vlen := val.Len()
slice := byteArrayBytes(val)
switch kind {
case Byte:
if vlen == 0 {
if len(slice) == 0 {
return &decodeError{msg: "input string too long", typ: val.Type()}
}
if vlen > 1 {
} else if len(slice) > 1 {
return &decodeError{msg: "input string too short", typ: val.Type()}
}
bv, _ := s.Uint()
val.Index(0).SetUint(bv)
slice[0] = s.byteval
s.kind = -1
case String:
if uint64(vlen) < size {
if uint64(len(slice)) < size {
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()}
}
slice := val.Slice(0, vlen).Interface().([]byte)
if err := s.readFull(slice); err != nil {
return err
}

@ -26,6 +26,8 @@ import (
"reflect"
"strings"
"testing"
"github.com/ethereum/go-ethereum/common/math"
)
func TestStreamKind(t *testing.T) {
@ -1063,7 +1065,7 @@ func ExampleStream() {
// [102 111 111 98 97 114] <nil>
}
func BenchmarkDecode(b *testing.B) {
func BenchmarkDecodeUints(b *testing.B) {
enc := encodeTestSlice(90000)
b.SetBytes(int64(len(enc)))
b.ReportAllocs()
@ -1078,7 +1080,7 @@ func BenchmarkDecode(b *testing.B) {
}
}
func BenchmarkDecodeIntSliceReuse(b *testing.B) {
func BenchmarkDecodeUintsReused(b *testing.B) {
enc := encodeTestSlice(100000)
b.SetBytes(int64(len(enc)))
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 {
s := make([]uint, n)
for i := uint(0); i < n; i++ {

@ -128,15 +128,11 @@ type encbuf struct {
lheads []listhead // all list headers
lhsize int // sum of sizes of all encoded list headers
sizebuf [9]byte // auxiliary buffer for uint encoding
bufvalue reflect.Value // used in writeByteArrayCopy
}
// encbufs are pooled.
var encbufPool = sync.Pool{
New: func() interface{} {
var bytes []byte
return &encbuf{bufvalue: reflect.ValueOf(&bytes).Elem()}
},
New: func() interface{} { return new(encbuf) },
}
func (w *encbuf) reset() {
@ -429,21 +425,14 @@ func writeBytes(val reflect.Value, w *encbuf) error {
return nil
}
var byteType = reflect.TypeOf(byte(0))
func makeByteArrayWriter(typ reflect.Type) writer {
length := typ.Len()
if length == 0 {
switch typ.Len() {
case 0:
return writeLengthZeroByteArray
} else if length == 1 {
case 1:
return writeLengthOneByteArray
}
if typ.Elem() != byteType {
return writeNamedByteArray
}
return func(val reflect.Value, w *encbuf) error {
writeByteArrayCopy(length, val, w)
return nil
default:
return writeByteArray
}
}
@ -462,29 +451,18 @@ func writeLengthOneByteArray(val reflect.Value, w *encbuf) error {
return nil
}
// writeByteArrayCopy encodes byte arrays using reflect.Copy. This is
// 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 {
func writeByteArray(val reflect.Value, w *encbuf) error {
if !val.CanAddr() {
// Slice requires the value to be addressable.
// Make it addressable by copying.
// Getting the byte slice of val requires it to be addressable. Make it
// addressable by copying.
copy := reflect.New(val.Type()).Elem()
copy.Set(val)
val = copy
}
size := val.Len()
slice := val.Slice(0, size).Bytes()
w.encodeString(slice)
slice := byteArrayBytes(val)
w.encodeStringHeader(len(slice))
w.str = append(w.str, slice...)
return nil
}

@ -513,3 +513,22 @@ func BenchmarkEncodeConcurrentInterface(b *testing.B) {
}
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
}