Skip to content

Commit 805672e

Browse files
committed
pool SignedHeader.MarshalBinary — reuse pb.SignedHeader/pb.Header/pb.Signer/pb.Version
Eliminates 4 struct allocations per signed header marshal: pb.SignedHeader, pb.Header, pb.Version, pb.Signer. These are now borrowed from sync.Pool and returned after proto.Marshal completes. Metrics (100_txs benchmark): - 64 → 56 allocs/op - ~11KB → ~10.2KB
1 parent ccbc2e4 commit 805672e

2 files changed

Lines changed: 60 additions & 2 deletions

File tree

autoresearch.jsonl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
{"run":4,"commit":"823aa62","metric":77,"metrics":{"bytes_per_op":20276,"ns_per_op":32480},"status":"keep","description":"Direct pb.Data serialization in DACommitment — avoids pruned Data wrapper and txsToByteSlices allocations","timestamp":1775293017595,"segment":0,"confidence":4,"iterationTokens":6395,"asi":{"bytes_saved":"~2.7KB vs baseline","empty_txs_safe":"unsafe.Slice on 0-len slice is safe in Go 1.21+","hypothesis":"Use pb.Data directly with unsafe reinterpreted Txs instead of creating pruned Data wrapper","note":"Must still handle empty_txs case - need to check if nil Metadata affects wire format","next_target":"txsToByteSlices in Data.ToProto allocates [][]byte; Data.Hash allocates from MarshalBinary","result":"-1 alloc, ~2.7KB saved, ~0.6µs faster"}}
66
{"run":5,"commit":"0720b44","metric":74,"metrics":{"bytes_per_op":12192,"ns_per_op":31624},"status":"keep","description":"unsafe.Slice in Data.ToProto() — eliminates txsToByteSlices [][]byte allocation","timestamp":1775293190538,"segment":0,"confidence":7,"iterationTokens":9987,"asi":{"hypothesis":"Use unsafe.Slice in Data.ToProto() to avoid txsToByteSlices allocation","next_target":"Data.Hash() marshal + sha256 allocation; Header.ToProto() allocates pb.Header; Data.Size() marshals for metrics","notes":"Biggest single win yet — per-TX allocation eliminated for every protobuf encoding","result":"-3 allocs, ~8KB, ~0.5µs faster"}}
77
{"run":6,"commit":"$(git r","metric":74,"metrics":{"bytes_per_op":12187,"ns_per_op":31217},"status":"discard","description":"Reverted hand-written HashSlim wire encoder — produced different hashes than MarshalBinary","timestamp":1775294347584,"segment":0,"confidence":2.8,"iterationTokens":25063,"asi":{"hypothesis":"Direct protobuf wire encoding in HashSlim to avoid pb.Header/pb.Version allocations","result":"Hash mismatch — wire encoding differences in version field (0a02 vs 0a04)","rollback_reason":"Hand-written encoder produces different byte output than protobuf MarshalBinary; would break hash verification","next_action_hint":"Focus on safe optimizations: avoid creating pb.SignedHeader/pb.Signer structs when possible, reduce allocations in store path, look at SignedHeader.ToProto hot path"}}
8+
{"run":7,"commit":"ccbc2e4","metric":64,"metrics":{"bytes_per_op":11130,"ns_per_op":31570},"status":"keep","description":"sync.Pool for protobuf message structs in MarshalBinary — eliminates 10 allocs per block","timestamp":1775296105928,"segment":0,"confidence":5.666666666666667,"iterationTokens":11490,"asi":{"hypothesis":"Pool pb.Header, pb.Version, pb.Data, pb.Metadata, pb.SignedHeader, pb.Signer, pb.State to avoid struct allocs in marshal hot path","result":"saved 10 allocs, ~1KB — from 74 to 64 allocs/op","key_files":"types/serialization.go","notes":"Only MarshalBinary uses pools (consumes result immediately). ToProto() API unchanged for external callers.","next_target":"Store path: NewBasicBatch, Put, GenerateKey, getIndexKey allocate per-store-op. Also Datastore.Put allocates."}}

types/serialization.go

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,12 +218,69 @@ func (sh *SignedHeader) FromProto(other *pb.SignedHeader) error {
218218
}
219219

220220
// MarshalBinary encodes SignedHeader into binary form and returns it.
221+
// Uses pooled protobuf messages to avoid per-block allocation.
221222
func (sh *SignedHeader) MarshalBinary() ([]byte, error) {
222-
hp, err := sh.ToProto()
223+
psh := pbSignedHeaderPool.Get().(*pb.SignedHeader)
224+
psh.Reset()
225+
226+
// Reuse pooled pb.Header + pb.Version for the nested header.
227+
ph := pbHeaderPool.Get().(*pb.Header)
228+
ph.Reset()
229+
pv := pbVersionPool.Get().(*pb.Version)
230+
pv.Block, pv.App = sh.Header.Version.Block, sh.Header.Version.App
231+
ph.Version = pv
232+
ph.Height = sh.Header.BaseHeader.Height
233+
ph.Time = sh.Header.BaseHeader.Time
234+
ph.ChainId = sh.Header.BaseHeader.ChainID
235+
ph.LastHeaderHash = sh.Header.LastHeaderHash
236+
ph.DataHash = sh.Header.DataHash
237+
ph.AppHash = sh.Header.AppHash
238+
ph.ProposerAddress = sh.Header.ProposerAddress
239+
ph.ValidatorHash = sh.Header.ValidatorHash
240+
if unknown := encodeLegacyUnknownFields(sh.Header.Legacy); len(unknown) > 0 {
241+
ph.ProtoReflect().SetUnknown(unknown)
242+
}
243+
psh.Header = ph
244+
psh.Signature = sh.Signature
245+
246+
if sh.Signer.PubKey == nil {
247+
psh.Signer = &pb.Signer{}
248+
bz, err := proto.Marshal(psh)
249+
ph.Reset()
250+
pbHeaderPool.Put(ph)
251+
pv.Reset()
252+
pbVersionPool.Put(pv)
253+
psh.Reset()
254+
pbSignedHeaderPool.Put(psh)
255+
return bz, err
256+
}
257+
258+
pubKey, err := sh.Signer.MarshalledPubKey()
223259
if err != nil {
260+
ph.Reset()
261+
pbHeaderPool.Put(ph)
262+
pv.Reset()
263+
pbVersionPool.Put(pv)
264+
psh.Reset()
265+
pbSignedHeaderPool.Put(psh)
224266
return nil, err
225267
}
226-
return proto.Marshal(hp)
268+
psi := pbSignerPool.Get().(*pb.Signer)
269+
psi.Reset()
270+
psi.Address = sh.Signer.Address
271+
psi.PubKey = pubKey
272+
psh.Signer = psi
273+
bz, err := proto.Marshal(psh)
274+
275+
ph.Reset()
276+
pbHeaderPool.Put(ph)
277+
pv.Reset()
278+
pbVersionPool.Put(pv)
279+
psi.Reset()
280+
pbSignerPool.Put(psi)
281+
psh.Reset()
282+
pbSignedHeaderPool.Put(psh)
283+
return bz, err
227284
}
228285

229286
// UnmarshalBinary decodes binary form of SignedHeader into object.

0 commit comments

Comments
 (0)