Skip to content

Commit 2cda86f

Browse files
committed
wire: Optimize writes to bytes.Buffer
This special cases writes to bytes.Buffer, which is always the writer type written to by WriteMessageN. There are several optimizations that can be implemented by special casing this type: First, pulling temporary short buffers from binary freelist can be skipped entirely, and instead the binary encoding of integers can be appended directly to its existing capacity. This avoids the synchronization cost to add and remove buffers from the free list, and for applications which only ever write wire messages with WriteMessageN, the allocation and ongoing garbage collection scanning cost to for these buffers can be completely skipped. Second, special casing the buffer type in WriteVarString saves us from creating a temporary heap copy of a string, as the buffer's WriteString method can be used instead. Third, special casing the buffer allows WriteMessageN to calculate the serialize size and grow its buffer so all remaining appends for writing block and transactions will not have to reallocate the buffer's backing allocation. This same optimization can be applied to other messages in the future.
1 parent 674d4b1 commit 2cda86f

File tree

8 files changed

+127
-91
lines changed

8 files changed

+127
-91
lines changed

wire/common.go

Lines changed: 97 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package wire
77

88
import (
9+
"bytes"
910
"crypto/rand"
1011
"encoding/binary"
1112
"fmt"
@@ -131,49 +132,6 @@ func (l binaryFreeList) Uint64(r io.Reader, byteOrder binary.ByteOrder) (uint64,
131132
return rv, nil
132133
}
133134

134-
// PutUint8 copies the provided uint8 into a buffer from the free list and
135-
// writes the resulting byte to the given writer.
136-
func (l binaryFreeList) PutUint8(w io.Writer, val uint8) error {
137-
buf := l.Borrow()[:1]
138-
buf[0] = val
139-
_, err := w.Write(buf)
140-
l.Return(buf)
141-
return err
142-
}
143-
144-
// PutUint16 serializes the provided uint16 using the given byte order into a
145-
// buffer from the free list and writes the resulting two bytes to the given
146-
// writer.
147-
func (l binaryFreeList) PutUint16(w io.Writer, byteOrder binary.ByteOrder, val uint16) error {
148-
buf := l.Borrow()[:2]
149-
byteOrder.PutUint16(buf, val)
150-
_, err := w.Write(buf)
151-
l.Return(buf)
152-
return err
153-
}
154-
155-
// PutUint32 serializes the provided uint32 using the given byte order into a
156-
// buffer from the free list and writes the resulting four bytes to the given
157-
// writer.
158-
func (l binaryFreeList) PutUint32(w io.Writer, byteOrder binary.ByteOrder, val uint32) error {
159-
buf := l.Borrow()[:4]
160-
byteOrder.PutUint32(buf, val)
161-
_, err := w.Write(buf)
162-
l.Return(buf)
163-
return err
164-
}
165-
166-
// PutUint64 serializes the provided uint64 using the given byte order into a
167-
// buffer from the free list and writes the resulting eight bytes to the given
168-
// writer.
169-
func (l binaryFreeList) PutUint64(w io.Writer, byteOrder binary.ByteOrder, val uint64) error {
170-
buf := l.Borrow()[:8]
171-
byteOrder.PutUint64(buf, val)
172-
_, err := w.Write(buf)
173-
l.Return(buf)
174-
return err
175-
}
176-
177135
// binarySerializer provides a free list of buffers to use for serializing and
178136
// deserializing primitive integer values to and from io.Readers and io.Writers.
179137
var binarySerializer binaryFreeList = make(chan []byte, binaryFreeListMaxItems)
@@ -412,48 +370,108 @@ func readElements(r io.Reader, elements ...interface{}) error {
412370
return nil
413371
}
414372

373+
// shortWrite optimizes short (<= 8 byte) writes to w by special casing
374+
// buffer allocations for specific writer types.
375+
//
376+
// The callback returns a short buffer to 8 bytes in length and a size
377+
// specifying how much of the buffer to write.
378+
//
379+
// For longer writes and writes of byte arrays, dynamic dispatch to w.Write
380+
// should be used instead.
381+
func shortWrite(w io.Writer, cb func() (data [8]byte, size int)) error {
382+
data, size := cb()
383+
384+
switch w := w.(type) {
385+
// The most common case (called through WriteMessageN) is that the writer is a
386+
// *bytes.Buffer. Optimize for that case by appending binary serializations
387+
// to its existing capacity instead of paying the synchronization cost to
388+
// serialize to temporary buffers pulled from the binary freelist.
389+
case *bytes.Buffer:
390+
w.Write(data[:size])
391+
return nil
392+
393+
default:
394+
p := binarySerializer.Borrow()[:size]
395+
copy(p, data[:size])
396+
_, err := w.Write(p)
397+
return err
398+
}
399+
}
400+
401+
// writeUint8 writes the byte value to the writer.
402+
func writeUint8(w io.Writer, value uint8) error {
403+
return shortWrite(w, func() (buf [8]byte, size int) {
404+
buf[0] = value
405+
return buf, 1
406+
})
407+
}
408+
409+
// writeUint16 writes the little endian encoding of value to the writer.
410+
func writeUint16(w io.Writer, value uint16) error {
411+
return shortWrite(w, func() (buf [8]byte, size int) {
412+
littleEndian.PutUint16(buf[:], value)
413+
return buf, 2
414+
})
415+
}
416+
417+
// writeUint32 writes the little endian encoding of value to the writer.
418+
func writeUint32(w io.Writer, value uint32) error {
419+
return shortWrite(w, func() (buf [8]byte, size int) {
420+
littleEndian.PutUint32(buf[:], value)
421+
return buf, 4
422+
})
423+
}
424+
425+
// writeUint64 writes the little endian encoding of value to the writer.
426+
func writeUint64(w io.Writer, value uint64) error {
427+
return shortWrite(w, func() (buf [8]byte, size int) {
428+
littleEndian.PutUint64(buf[:], value)
429+
return buf, 8
430+
})
431+
}
432+
415433
// writeElement writes the little endian representation of element to w.
416434
func writeElement(w io.Writer, element interface{}) error {
417435
// Attempt to write the element based on the concrete type via fast
418436
// type assertions first.
419437
switch e := element.(type) {
420438
case *uint8:
421-
err := binarySerializer.PutUint8(w, *e)
439+
err := writeUint8(w, *e)
422440
if err != nil {
423441
return err
424442
}
425443
return nil
426444

427445
case *uint16:
428-
err := binarySerializer.PutUint16(w, littleEndian, *e)
446+
err := writeUint16(w, *e)
429447
if err != nil {
430448
return err
431449
}
432450
return nil
433451

434452
case *int32:
435-
err := binarySerializer.PutUint32(w, littleEndian, uint32(*e))
453+
err := writeUint32(w, uint32(*e))
436454
if err != nil {
437455
return err
438456
}
439457
return nil
440458

441459
case *uint32:
442-
err := binarySerializer.PutUint32(w, littleEndian, *e)
460+
err := writeUint32(w, *e)
443461
if err != nil {
444462
return err
445463
}
446464
return nil
447465

448466
case *int64:
449-
err := binarySerializer.PutUint64(w, littleEndian, uint64(*e))
467+
err := writeUint64(w, uint64(*e))
450468
if err != nil {
451469
return err
452470
}
453471
return nil
454472

455473
case *uint64:
456-
err := binarySerializer.PutUint64(w, littleEndian, *e)
474+
err := writeUint64(w, *e)
457475
if err != nil {
458476
return err
459477
}
@@ -462,9 +480,9 @@ func writeElement(w io.Writer, element interface{}) error {
462480
case *bool:
463481
var err error
464482
if *e {
465-
err = binarySerializer.PutUint8(w, 0x01)
483+
err = writeUint8(w, 0x01)
466484
} else {
467-
err = binarySerializer.PutUint8(w, 0x00)
485+
err = writeUint8(w, 0x00)
468486
}
469487
if err != nil {
470488
return err
@@ -558,28 +576,28 @@ func writeElement(w io.Writer, element interface{}) error {
558576
return nil
559577

560578
case *ServiceFlag:
561-
err := binarySerializer.PutUint64(w, littleEndian, uint64(*e))
579+
err := writeUint64(w, uint64(*e))
562580
if err != nil {
563581
return err
564582
}
565583
return nil
566584

567585
case *InvType:
568-
err := binarySerializer.PutUint32(w, littleEndian, uint32(*e))
586+
err := writeUint32(w, uint32(*e))
569587
if err != nil {
570588
return err
571589
}
572590
return nil
573591

574592
case *CurrencyNet:
575-
err := binarySerializer.PutUint32(w, littleEndian, uint32(*e))
593+
err := writeUint32(w, uint32(*e))
576594
if err != nil {
577595
return err
578596
}
579597
return nil
580598

581599
case *RejectCode:
582-
err := binarySerializer.PutUint8(w, uint8(*e))
600+
err := writeUint8(w, uint8(*e))
583601
if err != nil {
584602
return err
585603
}
@@ -669,30 +687,34 @@ func ReadVarInt(r io.Reader, pver uint32) (uint64, error) {
669687
// on its value.
670688
func WriteVarInt(w io.Writer, pver uint32, val uint64) error {
671689
if val < 0xfd {
672-
return binarySerializer.PutUint8(w, uint8(val))
690+
return shortWrite(w, func() (p [8]byte, size int) {
691+
p[0] = uint8(val)
692+
return p, 1
693+
})
673694
}
674695

675696
if val <= math.MaxUint16 {
676-
err := binarySerializer.PutUint8(w, 0xfd)
677-
if err != nil {
678-
return err
679-
}
680-
return binarySerializer.PutUint16(w, littleEndian, uint16(val))
697+
return shortWrite(w, func() (p [8]byte, size int) {
698+
p[0] = 0xfd
699+
littleEndian.PutUint16(p[1:], uint16(val))
700+
return p, 3
701+
})
681702
}
682703

683704
if val <= math.MaxUint32 {
684-
err := binarySerializer.PutUint8(w, 0xfe)
685-
if err != nil {
686-
return err
687-
}
688-
return binarySerializer.PutUint32(w, littleEndian, uint32(val))
705+
return shortWrite(w, func() (p [8]byte, size int) {
706+
p[0] = 0xfe
707+
littleEndian.PutUint32(p[1:], uint32(val))
708+
return p, 5
709+
})
689710
}
690711

691-
err := binarySerializer.PutUint8(w, 0xff)
712+
// shortWrite is not designed for writes > 8 bytes.
713+
err := writeUint8(w, 0xff)
692714
if err != nil {
693715
return err
694716
}
695-
return binarySerializer.PutUint64(w, littleEndian, val)
717+
return writeUint64(w, val)
696718
}
697719

698720
// VarIntSerializeSize returns the number of bytes it would take to serialize
@@ -798,7 +820,13 @@ func WriteVarString(w io.Writer, pver uint32, str string) error {
798820
if err != nil {
799821
return err
800822
}
801-
_, err = w.Write([]byte(str))
823+
824+
switch w := w.(type) {
825+
case *bytes.Buffer:
826+
_, err = w.WriteString(str)
827+
default:
828+
_, err = w.Write([]byte(str))
829+
}
802830
return err
803831
}
804832

wire/message.go

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -306,14 +306,23 @@ func WriteMessageN(w io.Writer, msg Message, pver uint32, dcrnet CurrencyNet) (i
306306
}
307307
copy(elems.command[:], []byte(cmd))
308308

309+
// Allocate enough buffer space for the entire message size if it is
310+
// known. When it is not known, use an extra size hint of 64 bytes,
311+
// which matches the default small allocation size of a bytes.Buffer
312+
// as of Go 1.25.
313+
extraCap := 64
314+
switch msg := msg.(type) {
315+
case interface{ SerializeSize() int }:
316+
extraCap = msg.SerializeSize()
317+
}
318+
309319
// Initialize buffer with zeroed bytes for the message header (to be
310320
// filled in, with checksum, after appending the payload
311-
// serialization).
312-
var buf bytes.Buffer
313-
buf.Write(make([]byte, MessageHeaderSize))
321+
// serialization), plus additional capacity for writing the payload.
322+
buf := bytes.NewBuffer(make([]byte, MessageHeaderSize, MessageHeaderSize+extraCap))
314323

315324
// Encode the message payload.
316-
err := msg.BtcEncode(&buf, pver)
325+
err := msg.BtcEncode(buf, pver)
317326
if err != nil {
318327
return 0, err
319328
}
@@ -342,7 +351,7 @@ func WriteMessageN(w io.Writer, msg Message, pver uint32, dcrnet CurrencyNet) (i
342351
cksumHash := chainhash.HashH(payload)
343352
copy(elems.checksum[:], cksumHash[0:4])
344353
buf.Reset()
345-
writeElements(&buf, &elems.dcrnet, &elems.command, &elems.lenp, &elems.checksum)
354+
writeElements(buf, &elems.dcrnet, &elems.command, &elems.lenp, &elems.checksum)
346355
if buf.Len() != MessageHeaderSize {
347356
// The length of data written for the header is always
348357
// constant, is not dependent on the message being serialized,

wire/msgcfheaders.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ func (msg *MsgCFHeaders) BtcEncode(w io.Writer, pver uint32) error {
116116
}
117117

118118
// Write filter type
119-
err = binarySerializer.PutUint8(w, uint8(msg.FilterType))
119+
err = writeUint8(w, uint8(msg.FilterType))
120120
if err != nil {
121121
return err
122122
}

wire/msgcfilter.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ func (msg *MsgCFilter) BtcEncode(w io.Writer, pver uint32) error {
8080
return err
8181
}
8282

83-
err = binarySerializer.PutUint8(w, uint8(msg.FilterType))
83+
err = writeUint8(w, uint8(msg.FilterType))
8484
if err != nil {
8585
return err
8686
}

wire/msgcftypes.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ func (msg *MsgCFTypes) BtcEncode(w io.Writer, pver uint32) error {
9292
}
9393

9494
for i := range msg.SupportedFilters {
95-
err = binarySerializer.PutUint8(w, uint8(msg.SupportedFilters[i]))
95+
err = writeUint8(w, uint8(msg.SupportedFilters[i]))
9696
if err != nil {
9797
return err
9898
}

wire/msggetcfheaders.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ func (msg *MsgGetCFHeaders) BtcEncode(w io.Writer, pver uint32) error {
115115
return err
116116
}
117117

118-
return binarySerializer.PutUint8(w, uint8(msg.FilterType))
118+
return writeUint8(w, uint8(msg.FilterType))
119119
}
120120

121121
// Command returns the protocol command string for the message. This is part

wire/msggetcfilter.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ func (msg *MsgGetCFilter) BtcEncode(w io.Writer, pver uint32) error {
5454
if err != nil {
5555
return err
5656
}
57-
return binarySerializer.PutUint8(w, uint8(msg.FilterType))
57+
return writeUint8(w, uint8(msg.FilterType))
5858
}
5959

6060
// Command returns the protocol command string for the message. This is part

0 commit comments

Comments
 (0)