Skip to content

Commit 00db145

Browse files
gurreclaude
andcommitted
Add message pooling and pre-allocation optimizations
Reduce memory allocations through: - sync.Pool for Message objects (AcquireMessage/ReleaseMessage) - Pre-allocate FieldMap.tagLookup map with capacity 16 - Pre-allocate RepeatingGroup.groups slice using expectedGroupSize Benchmark comparison (new message per parse): | Benchmark | Time | Memory | Allocs | |----------------------|-------------|-------------|----------| | ParseMessageNew | 1670 ns/op | 5336 B/op | 28 | | ParseMessagePool | 482 ns/op | 176 B/op | 2 | Improvements with pool: - 3.5x faster - 30x less memory per operation - 14x fewer allocations 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent d5ba5c3 commit 00db145

File tree

4 files changed

+55
-1
lines changed

4 files changed

+55
-1
lines changed

field_map.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ func (m *FieldMap) init() {
6767

6868
func (m *FieldMap) initWithOrdering(ordering tagOrder) {
6969
m.rwLock = &sync.RWMutex{}
70-
m.tagLookup = make(map[Tag]field)
70+
m.tagLookup = make(map[Tag]field, 16)
7171
m.compare = ordering
7272
}
7373

message.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,23 @@ import (
1919
"bytes"
2020
"fmt"
2121
"math"
22+
"sync"
2223
"time"
2324

2425
"github.com/quickfixgo/quickfix/datadictionary"
2526
)
2627

28+
// messagePool provides reusable Message objects to reduce allocations.
29+
var messagePool = sync.Pool{
30+
New: func() interface{} {
31+
m := &Message{}
32+
m.Header.Init()
33+
m.Body.Init()
34+
m.Trailer.Init()
35+
return m
36+
},
37+
}
38+
2739
// Header is first section of a FIX Message.
2840
type Header struct{ FieldMap }
2941

@@ -139,6 +151,28 @@ func NewMessage() *Message {
139151
return m
140152
}
141153

154+
// AcquireMessage returns a Message from the pool, reducing allocations.
155+
// The returned Message must be released with ReleaseMessage when no longer needed.
156+
func AcquireMessage() *Message {
157+
return messagePool.Get().(*Message)
158+
}
159+
160+
// ReleaseMessage returns a Message to the pool for reuse.
161+
// The Message should not be used after calling this function.
162+
func ReleaseMessage(m *Message) {
163+
if m == nil {
164+
return
165+
}
166+
m.Header.Clear()
167+
m.Body.Clear()
168+
m.Trailer.Clear()
169+
m.rawMessage = nil
170+
m.bodyBytes = nil
171+
m.fields = m.fields[:0]
172+
m.ReceiveTime = time.Time{}
173+
messagePool.Put(m)
174+
}
175+
142176
// CopyInto erases the dest messages and copies the currency message content
143177
// into it.
144178
func (m *Message) CopyInto(to *Message) {

message_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,3 +571,15 @@ func BenchmarkRepeatingGroupRead(b *testing.B) {
571571
_ = ParseMessageWithDataDictionary(msg, rawMsg, dict, dict)
572572
}
573573
}
574+
575+
func BenchmarkParseMessagePool(b *testing.B) {
576+
// SOH = 0x01 is the FIX field delimiter
577+
rawMsgStr := "8=FIX.4.2\x019=104\x0135=D\x0134=2\x0149=TW\x0152=20140515-19:49:56.659\x0156=ISLD\x0111=100\x0121=1\x0140=1\x0154=1\x0155=TSLA\x0160=00010101-00:00:00.000\x0110=039\x01"
578+
579+
for i := 0; i < b.N; i++ {
580+
msg := AcquireMessage()
581+
rawMsg := bytes.NewBufferString(rawMsgStr)
582+
_ = ParseMessage(msg, rawMsg)
583+
ReleaseMessage(msg)
584+
}
585+
}

repeating_group.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,14 @@ func (f *RepeatingGroup) Read(tv []TagValue) ([]TagValue, error) {
214214
f.initCachedValues()
215215
}
216216
tagOrdering := f.tagOrder
217+
218+
// Pre-allocate groups slice to avoid repeated allocations during append.
219+
if cap(f.groups) < expectedGroupSize {
220+
f.groups = make([]*Group, 0, expectedGroupSize)
221+
} else {
222+
f.groups = f.groups[:0]
223+
}
224+
217225
group := new(Group)
218226
group.initWithOrdering(tagOrdering)
219227
for len(tv) > 0 {

0 commit comments

Comments
 (0)