rlp: optimize big.Int decoding for size <= 32 bytes (#22927)

This change grows the static integer buffer in Stream to 32 bytes,
making it possible to decode 256bit integers without allocating a
temporary buffer.

In the recent commit 088da24, Stream struct size decreased from 120
bytes down to 88 bytes. This commit grows the struct to 112 bytes again,
but the size change will not degrade performance because Stream
instances are internally cached in sync.Pool.

    name             old time/op    new time/op    delta
    DecodeBigInts-8    12.2µs ± 0%     8.6µs ± 4%  -29.58%  (p=0.000 n=9+10)

    name             old speed      new speed      delta
    DecodeBigInts-8   230MB/s ± 0%   326MB/s ± 4%  +42.04%  (p=0.000 n=9+10)
This commit is contained in:
Felix Lange 2021-05-25 21:56:25 +02:00 committed by GitHub
parent 017cf71fbd
commit 4d33de9b49
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 74 additions and 16 deletions

View File

@ -220,20 +220,51 @@ func decodeBigIntNoPtr(s *Stream, val reflect.Value) error {
} }
func decodeBigInt(s *Stream, val reflect.Value) error { func decodeBigInt(s *Stream, val reflect.Value) error {
b, err := s.Bytes() var buffer []byte
if err != nil { kind, size, err := s.Kind()
switch {
case err != nil:
return wrapStreamError(err, val.Type()) return wrapStreamError(err, val.Type())
case kind == List:
return wrapStreamError(ErrExpectedString, val.Type())
case kind == Byte:
buffer = s.uintbuf[:1]
buffer[0] = s.byteval
s.kind = -1 // re-arm Kind
case size == 0:
// Avoid zero-length read.
s.kind = -1
case size <= uint64(len(s.uintbuf)):
// For integers smaller than s.uintbuf, allocating a buffer
// can be avoided.
buffer = s.uintbuf[:size]
if err := s.readFull(buffer); err != nil {
return wrapStreamError(err, val.Type())
}
// Reject inputs where single byte encoding should have been used.
if size == 1 && buffer[0] < 128 {
return wrapStreamError(ErrCanonSize, val.Type())
}
default:
// For large integers, a temporary buffer is needed.
buffer = make([]byte, size)
if err := s.readFull(buffer); err != nil {
return wrapStreamError(err, val.Type())
}
} }
// Reject leading zero bytes.
if len(buffer) > 0 && buffer[0] == 0 {
return wrapStreamError(ErrCanonInt, val.Type())
}
// Set the integer bytes.
i := val.Interface().(*big.Int) i := val.Interface().(*big.Int)
if i == nil { if i == nil {
i = new(big.Int) i = new(big.Int)
val.Set(reflect.ValueOf(i)) val.Set(reflect.ValueOf(i))
} }
// Reject leading zero bytes. i.SetBytes(buffer)
if len(b) > 0 && b[0] == 0 {
return wrapStreamError(ErrCanonInt, val.Type())
}
i.SetBytes(b)
return nil return nil
} }
@ -563,7 +594,7 @@ type Stream struct {
size uint64 // size of value ahead size uint64 // size of value ahead
kinderr error // error from last readKind kinderr error // error from last readKind
stack []uint64 // list sizes stack []uint64 // list sizes
uintbuf [8]byte // auxiliary buffer for integer decoding uintbuf [32]byte // auxiliary buffer for integer decoding
kind Kind // kind of value ahead kind Kind // kind of value ahead
byteval byte // value of single byte in type tag byteval byte // value of single byte in type tag
limited bool // true if input limit is in effect limited bool // true if input limit is in effect
@ -817,7 +848,7 @@ func (s *Stream) Reset(r io.Reader, inputLimit uint64) {
s.kind = -1 s.kind = -1
s.kinderr = nil s.kinderr = nil
s.byteval = 0 s.byteval = 0
s.uintbuf = [8]byte{} s.uintbuf = [32]byte{}
} }
// Kind returns the kind and size of the next value in the // Kind returns the kind and size of the next value in the
@ -927,17 +958,20 @@ func (s *Stream) readUint(size byte) (uint64, error) {
b, err := s.readByte() b, err := s.readByte()
return uint64(b), err return uint64(b), err
default: default:
buffer := s.uintbuf[:8]
for i := range buffer {
buffer[i] = 0
}
start := int(8 - size) start := int(8 - size)
s.uintbuf = [8]byte{} if err := s.readFull(buffer[start:]); err != nil {
if err := s.readFull(s.uintbuf[start:]); err != nil {
return 0, err return 0, err
} }
if s.uintbuf[start] == 0 { if buffer[start] == 0 {
// Note: readUint is also used to decode integer values. // Note: readUint is also used to decode integer values.
// The error needs to be adjusted to become ErrCanonInt in this case. // The error needs to be adjusted to become ErrCanonInt in this case.
return 0, ErrCanonSize return 0, ErrCanonSize
} }
return binary.BigEndian.Uint64(s.uintbuf[:]), nil return binary.BigEndian.Uint64(buffer[:]), nil
} }
} }

View File

@ -329,6 +329,11 @@ type recstruct struct {
Child *recstruct `rlp:"nil"` Child *recstruct `rlp:"nil"`
} }
type bigIntStruct struct {
I *big.Int
B string
}
type invalidNilTag struct { type invalidNilTag struct {
X []byte `rlp:"nil"` X []byte `rlp:"nil"`
} }
@ -405,10 +410,11 @@ type ignoredField struct {
} }
var ( var (
veryBigInt = big.NewInt(0).Add( veryBigInt = new(big.Int).Add(
big.NewInt(0).Lsh(big.NewInt(0xFFFFFFFFFFFFFF), 16), big.NewInt(0).Lsh(big.NewInt(0xFFFFFFFFFFFFFF), 16),
big.NewInt(0xFFFF), big.NewInt(0xFFFF),
) )
veryVeryBigInt = new(big.Int).Exp(veryBigInt, big.NewInt(8), nil)
) )
var decodeTests = []decodeTest{ var decodeTests = []decodeTest{
@ -479,12 +485,15 @@ var decodeTests = []decodeTest{
{input: "C0", ptr: new(string), error: "rlp: expected input string or byte for string"}, {input: "C0", ptr: new(string), error: "rlp: expected input string or byte for string"},
// big ints // big ints
{input: "80", ptr: new(*big.Int), value: big.NewInt(0)},
{input: "01", ptr: new(*big.Int), value: big.NewInt(1)}, {input: "01", ptr: new(*big.Int), value: big.NewInt(1)},
{input: "89FFFFFFFFFFFFFFFFFF", ptr: new(*big.Int), value: veryBigInt}, {input: "89FFFFFFFFFFFFFFFFFF", ptr: new(*big.Int), value: veryBigInt},
{input: "B848FFFFFFFFFFFFFFFFF800000000000000001BFFFFFFFFFFFFFFFFC8000000000000000045FFFFFFFFFFFFFFFFC800000000000000001BFFFFFFFFFFFFFFFFF8000000000000000001", ptr: new(*big.Int), value: veryVeryBigInt},
{input: "10", ptr: new(big.Int), value: *big.NewInt(16)}, // non-pointer also works {input: "10", ptr: new(big.Int), value: *big.NewInt(16)}, // non-pointer also works
{input: "C0", ptr: new(*big.Int), error: "rlp: expected input string or byte for *big.Int"}, {input: "C0", ptr: new(*big.Int), error: "rlp: expected input string or byte for *big.Int"},
{input: "820001", ptr: new(big.Int), error: "rlp: non-canonical integer (leading zero bytes) for *big.Int"}, {input: "00", ptr: new(*big.Int), error: "rlp: non-canonical integer (leading zero bytes) for *big.Int"},
{input: "8105", ptr: new(big.Int), error: "rlp: non-canonical size information for *big.Int"}, {input: "820001", ptr: new(*big.Int), error: "rlp: non-canonical integer (leading zero bytes) for *big.Int"},
{input: "8105", ptr: new(*big.Int), error: "rlp: non-canonical size information for *big.Int"},
// structs // structs
{ {
@ -497,6 +506,13 @@ var decodeTests = []decodeTest{
ptr: new(recstruct), ptr: new(recstruct),
value: recstruct{1, &recstruct{2, &recstruct{3, nil}}}, value: recstruct{1, &recstruct{2, &recstruct{3, nil}}},
}, },
{
// This checks that empty big.Int works correctly in struct context. It's easy to
// miss the update of s.kind for this case, so it needs its own test.
input: "C58083343434",
ptr: new(bigIntStruct),
value: bigIntStruct{new(big.Int), "444"},
},
// struct errors // struct errors
{ {

View File

@ -131,6 +131,14 @@ var encTests = []encTest{
val: big.NewInt(0).SetBytes(unhex("010000000000000000000000000000000000000000000000000000000000000000")), val: big.NewInt(0).SetBytes(unhex("010000000000000000000000000000000000000000000000000000000000000000")),
output: "A1010000000000000000000000000000000000000000000000000000000000000000", output: "A1010000000000000000000000000000000000000000000000000000000000000000",
}, },
{
val: veryBigInt,
output: "89FFFFFFFFFFFFFFFFFF",
},
{
val: veryVeryBigInt,
output: "B848FFFFFFFFFFFFFFFFF800000000000000001BFFFFFFFFFFFFFFFFC8000000000000000045FFFFFFFFFFFFFFFFC800000000000000001BFFFFFFFFFFFFFFFFF8000000000000000001",
},
// non-pointer big.Int // non-pointer big.Int
{val: *big.NewInt(0), output: "80"}, {val: *big.NewInt(0), output: "80"},