Skip to content

Commit 6315b6f

Browse files
authored
rlp: reduce allocations for big.Int and byte array encoding (ethereum#21291)
This change further improves the performance of RLP encoding by removing allocations for big.Int and [...]byte types. I have added a new benchmark that measures RLP encoding of types.Block to verify that performance is improved.
1 parent fa01117 commit 6315b6f

File tree

4 files changed

+222
-43
lines changed

4 files changed

+222
-43
lines changed

core/types/block_test.go

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ import (
2323
"testing"
2424

2525
"github.com/ethereum/go-ethereum/common"
26+
"github.com/ethereum/go-ethereum/common/math"
27+
"github.com/ethereum/go-ethereum/crypto"
28+
"github.com/ethereum/go-ethereum/params"
2629
"github.com/ethereum/go-ethereum/rlp"
2730
)
2831

@@ -72,10 +75,58 @@ func TestUncleHash(t *testing.T) {
7275
t.Fatalf("empty uncle hash is wrong, got %x != %x", h, exp)
7376
}
7477
}
75-
func BenchmarkUncleHash(b *testing.B) {
76-
uncles := make([]*Header, 0)
78+
79+
var benchBuffer = bytes.NewBuffer(make([]byte, 0, 32000))
80+
81+
func BenchmarkEncodeBlock(b *testing.B) {
82+
block := makeBenchBlock()
7783
b.ResetTimer()
84+
7885
for i := 0; i < b.N; i++ {
79-
CalcUncleHash(uncles)
86+
benchBuffer.Reset()
87+
if err := rlp.Encode(benchBuffer, block); err != nil {
88+
b.Fatal(err)
89+
}
90+
}
91+
}
92+
93+
func makeBenchBlock() *Block {
94+
var (
95+
key, _ = crypto.GenerateKey()
96+
txs = make([]*Transaction, 70)
97+
receipts = make([]*Receipt, len(txs))
98+
signer = NewEIP155Signer(params.TestChainConfig.ChainID)
99+
uncles = make([]*Header, 3)
100+
)
101+
header := &Header{
102+
Difficulty: math.BigPow(11, 11),
103+
Number: math.BigPow(2, 9),
104+
GasLimit: 12345678,
105+
GasUsed: 1476322,
106+
Time: 9876543,
107+
Extra: []byte("coolest block on chain"),
108+
}
109+
for i := range txs {
110+
amount := math.BigPow(2, int64(i))
111+
price := big.NewInt(300000)
112+
data := make([]byte, 100)
113+
tx := NewTransaction(uint64(i), common.Address{}, amount, 123457, price, data)
114+
signedTx, err := SignTx(tx, signer, key)
115+
if err != nil {
116+
panic(err)
117+
}
118+
txs[i] = signedTx
119+
receipts[i] = NewReceipt(make([]byte, 32), false, tx.Gas())
120+
}
121+
for i := range uncles {
122+
uncles[i] = &Header{
123+
Difficulty: math.BigPow(11, 11),
124+
Number: math.BigPow(2, 9),
125+
GasLimit: 12345678,
126+
GasUsed: 1476322,
127+
Time: 9876543,
128+
Extra: []byte("benchmark uncle"),
129+
}
80130
}
131+
return NewBlock(header, txs, uncles, receipts)
81132
}

rlp/encode.go

Lines changed: 100 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -91,13 +91,6 @@ func EncodeToReader(val interface{}) (size int, r io.Reader, err error) {
9191
return eb.size(), &encReader{buf: eb}, nil
9292
}
9393

94-
type encbuf struct {
95-
str []byte // string data, contains everything except list headers
96-
lheads []listhead // all list headers
97-
lhsize int // sum of sizes of all encoded list headers
98-
sizebuf []byte // 9-byte auxiliary buffer for uint encoding
99-
}
100-
10194
type listhead struct {
10295
offset int // index of this header in string data
10396
size int // total size of encoded data (including list headers)
@@ -130,9 +123,20 @@ func puthead(buf []byte, smalltag, largetag byte, size uint64) int {
130123
return sizesize + 1
131124
}
132125

126+
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
132+
}
133+
133134
// encbufs are pooled.
134135
var encbufPool = sync.Pool{
135-
New: func() interface{} { return &encbuf{sizebuf: make([]byte, 9)} },
136+
New: func() interface{} {
137+
var bytes []byte
138+
return &encbuf{bufvalue: reflect.ValueOf(&bytes).Elem()}
139+
},
136140
}
137141

138142
func (w *encbuf) reset() {
@@ -160,7 +164,6 @@ func (w *encbuf) encodeStringHeader(size int) {
160164
if size < 56 {
161165
w.str = append(w.str, 0x80+byte(size))
162166
} else {
163-
// TODO: encode to w.str directly
164167
sizesize := putint(w.sizebuf[1:], uint64(size))
165168
w.sizebuf[0] = 0xB7 + byte(sizesize)
166169
w.str = append(w.str, w.sizebuf[:sizesize+1]...)
@@ -177,6 +180,19 @@ func (w *encbuf) encodeString(b []byte) {
177180
}
178181
}
179182

183+
func (w *encbuf) encodeUint(i uint64) {
184+
if i == 0 {
185+
w.str = append(w.str, 0x80)
186+
} else if i < 128 {
187+
// fits single byte
188+
w.str = append(w.str, byte(i))
189+
} else {
190+
s := putint(w.sizebuf[1:], i)
191+
w.sizebuf[0] = 0x80 + byte(s)
192+
w.str = append(w.str, w.sizebuf[:s+1]...)
193+
}
194+
}
195+
180196
// list adds a new list header to the header stack. It returns the index
181197
// of the header. The caller must call listEnd with this index after encoding
182198
// the content of the list.
@@ -229,7 +245,7 @@ func (w *encbuf) toWriter(out io.Writer) (err error) {
229245
}
230246
}
231247
// write the header
232-
enc := head.encode(w.sizebuf)
248+
enc := head.encode(w.sizebuf[:])
233249
if _, err = out.Write(enc); err != nil {
234250
return err
235251
}
@@ -295,7 +311,7 @@ func (r *encReader) next() []byte {
295311
return p
296312
}
297313
r.lhpos++
298-
return head.encode(r.buf.sizebuf)
314+
return head.encode(r.buf.sizebuf[:])
299315

300316
case r.strpos < len(r.buf.str):
301317
// String data at the end, after all list headers.
@@ -308,10 +324,7 @@ func (r *encReader) next() []byte {
308324
}
309325
}
310326

311-
var (
312-
encoderInterface = reflect.TypeOf(new(Encoder)).Elem()
313-
big0 = big.NewInt(0)
314-
)
327+
var encoderInterface = reflect.TypeOf(new(Encoder)).Elem()
315328

316329
// makeWriter creates a writer function for the given type.
317330
func makeWriter(typ reflect.Type, ts tags) (writer, error) {
@@ -336,7 +349,7 @@ func makeWriter(typ reflect.Type, ts tags) (writer, error) {
336349
case kind == reflect.Slice && isByte(typ.Elem()):
337350
return writeBytes, nil
338351
case kind == reflect.Array && isByte(typ.Elem()):
339-
return writeByteArray, nil
352+
return makeByteArrayWriter(typ), nil
340353
case kind == reflect.Slice || kind == reflect.Array:
341354
return makeSliceWriter(typ, ts)
342355
case kind == reflect.Struct:
@@ -348,28 +361,13 @@ func makeWriter(typ reflect.Type, ts tags) (writer, error) {
348361
}
349362
}
350363

351-
func isByte(typ reflect.Type) bool {
352-
return typ.Kind() == reflect.Uint8 && !typ.Implements(encoderInterface)
353-
}
354-
355364
func writeRawValue(val reflect.Value, w *encbuf) error {
356365
w.str = append(w.str, val.Bytes()...)
357366
return nil
358367
}
359368

360369
func writeUint(val reflect.Value, w *encbuf) error {
361-
i := val.Uint()
362-
if i == 0 {
363-
w.str = append(w.str, 0x80)
364-
} else if i < 128 {
365-
// fits single byte
366-
w.str = append(w.str, byte(i))
367-
} else {
368-
// TODO: encode int to w.str directly
369-
s := putint(w.sizebuf[1:], i)
370-
w.sizebuf[0] = 0x80 + byte(s)
371-
w.str = append(w.str, w.sizebuf[:s+1]...)
372-
}
370+
w.encodeUint(val.Uint())
373371
return nil
374372
}
375373

@@ -396,13 +394,32 @@ func writeBigIntNoPtr(val reflect.Value, w *encbuf) error {
396394
return writeBigInt(&i, w)
397395
}
398396

397+
// wordBytes is the number of bytes in a big.Word
398+
const wordBytes = (32 << (uint64(^big.Word(0)) >> 63)) / 8
399+
399400
func writeBigInt(i *big.Int, w *encbuf) error {
400-
if cmp := i.Cmp(big0); cmp == -1 {
401+
if i.Sign() == -1 {
401402
return fmt.Errorf("rlp: cannot encode negative *big.Int")
402-
} else if cmp == 0 {
403-
w.str = append(w.str, 0x80)
404-
} else {
405-
w.encodeString(i.Bytes())
403+
}
404+
bitlen := i.BitLen()
405+
if bitlen <= 64 {
406+
w.encodeUint(i.Uint64())
407+
return nil
408+
}
409+
// Integer is larger than 64 bits, encode from i.Bits().
410+
// The minimal byte length is bitlen rounded up to the next
411+
// multiple of 8, divided by 8.
412+
length := ((bitlen + 7) & -8) >> 3
413+
w.encodeStringHeader(length)
414+
w.str = append(w.str, make([]byte, length)...)
415+
index := length
416+
buf := w.str[len(w.str)-length:]
417+
for _, d := range i.Bits() {
418+
for j := 0; j < wordBytes && index > 0; j++ {
419+
index--
420+
buf[index] = byte(d)
421+
d >>= 8
422+
}
406423
}
407424
return nil
408425
}
@@ -412,7 +429,52 @@ func writeBytes(val reflect.Value, w *encbuf) error {
412429
return nil
413430
}
414431

415-
func writeByteArray(val reflect.Value, w *encbuf) error {
432+
var byteType = reflect.TypeOf(byte(0))
433+
434+
func makeByteArrayWriter(typ reflect.Type) writer {
435+
length := typ.Len()
436+
if length == 0 {
437+
return writeLengthZeroByteArray
438+
} else if length == 1 {
439+
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
447+
}
448+
}
449+
450+
func writeLengthZeroByteArray(val reflect.Value, w *encbuf) error {
451+
w.str = append(w.str, 0x80)
452+
return nil
453+
}
454+
455+
func writeLengthOneByteArray(val reflect.Value, w *encbuf) error {
456+
b := byte(val.Index(0).Uint())
457+
if b <= 0x7f {
458+
w.str = append(w.str, b)
459+
} else {
460+
w.str = append(w.str, 0x81, b)
461+
}
462+
return nil
463+
}
464+
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 {
416478
if !val.CanAddr() {
417479
// Slice requires the value to be addressable.
418480
// Make it addressable by copying.

rlp/encode_test.go

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import (
2525
"math/big"
2626
"sync"
2727
"testing"
28+
29+
"github.com/ethereum/go-ethereum/common/math"
2830
)
2931

3032
type testEncoder struct {
@@ -137,16 +139,43 @@ var encTests = []encTest{
137139
// negative ints are not supported
138140
{val: big.NewInt(-1), error: "rlp: cannot encode negative *big.Int"},
139141

140-
// byte slices, strings
142+
// byte arrays
143+
{val: [0]byte{}, output: "80"},
144+
{val: [1]byte{0}, output: "00"},
145+
{val: [1]byte{1}, output: "01"},
146+
{val: [1]byte{0x7F}, output: "7F"},
147+
{val: [1]byte{0x80}, output: "8180"},
148+
{val: [1]byte{0xFF}, output: "81FF"},
149+
{val: [3]byte{1, 2, 3}, output: "83010203"},
150+
{val: [57]byte{1, 2, 3}, output: "B839010203000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"},
151+
152+
// named byte type arrays
153+
{val: [0]namedByteType{}, output: "80"},
154+
{val: [1]namedByteType{0}, output: "00"},
155+
{val: [1]namedByteType{1}, output: "01"},
156+
{val: [1]namedByteType{0x7F}, output: "7F"},
157+
{val: [1]namedByteType{0x80}, output: "8180"},
158+
{val: [1]namedByteType{0xFF}, output: "81FF"},
159+
{val: [3]namedByteType{1, 2, 3}, output: "83010203"},
160+
{val: [57]namedByteType{1, 2, 3}, output: "B839010203000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"},
161+
162+
// byte slices
141163
{val: []byte{}, output: "80"},
164+
{val: []byte{0}, output: "00"},
142165
{val: []byte{0x7E}, output: "7E"},
143166
{val: []byte{0x7F}, output: "7F"},
144167
{val: []byte{0x80}, output: "8180"},
145168
{val: []byte{1, 2, 3}, output: "83010203"},
146169

170+
// named byte type slices
171+
{val: []namedByteType{}, output: "80"},
172+
{val: []namedByteType{0}, output: "00"},
173+
{val: []namedByteType{0x7E}, output: "7E"},
174+
{val: []namedByteType{0x7F}, output: "7F"},
175+
{val: []namedByteType{0x80}, output: "8180"},
147176
{val: []namedByteType{1, 2, 3}, output: "83010203"},
148-
{val: [...]namedByteType{1, 2, 3}, output: "83010203"},
149177

178+
// strings
150179
{val: "", output: "80"},
151180
{val: "\x7E", output: "7E"},
152181
{val: "\x7F", output: "7F"},
@@ -401,3 +430,36 @@ func TestEncodeToReaderReturnToPool(t *testing.T) {
401430
}
402431
wg.Wait()
403432
}
433+
434+
var sink interface{}
435+
436+
func BenchmarkIntsize(b *testing.B) {
437+
for i := 0; i < b.N; i++ {
438+
sink = intsize(0x12345678)
439+
}
440+
}
441+
442+
func BenchmarkPutint(b *testing.B) {
443+
buf := make([]byte, 8)
444+
for i := 0; i < b.N; i++ {
445+
putint(buf, 0x12345678)
446+
sink = buf
447+
}
448+
}
449+
450+
func BenchmarkEncodeBigInts(b *testing.B) {
451+
ints := make([]*big.Int, 200)
452+
for i := range ints {
453+
ints[i] = math.BigPow(2, int64(i))
454+
}
455+
out := bytes.NewBuffer(make([]byte, 0, 4096))
456+
b.ResetTimer()
457+
b.ReportAllocs()
458+
459+
for i := 0; i < b.N; i++ {
460+
out.Reset()
461+
if err := Encode(out, ints); err != nil {
462+
b.Fatal(err)
463+
}
464+
}
465+
}

rlp/typecache.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,10 @@ func isUint(k reflect.Kind) bool {
210210
return k >= reflect.Uint && k <= reflect.Uintptr
211211
}
212212

213+
func isByte(typ reflect.Type) bool {
214+
return typ.Kind() == reflect.Uint8 && !typ.Implements(encoderInterface)
215+
}
216+
213217
func isByteArray(typ reflect.Type) bool {
214218
return (typ.Kind() == reflect.Slice || typ.Kind() == reflect.Array) && isByte(typ.Elem())
215219
}

0 commit comments

Comments
 (0)