Skip to content

Commit 857893e

Browse files
authored
refactor: optimize parseResult and scanResult pooling for improved me… (#51)
* refactor: optimize parseResult and scanResult pooling for improved memory efficiency and performance * chore: fix lint error
1 parent b5ae996 commit 857893e

3 files changed

Lines changed: 36 additions & 14 deletions

File tree

field_parser.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,21 @@ type parseResult struct {
2323
rows []rowInfo // All row information
2424
}
2525

26+
// parseResultPoolFieldCap is the initial field capacity for pooled parseResult.
27+
// 4096 fields covers ~800 rows with 5 fields each (typical CSV).
28+
// fieldInfo is 12 bytes, so 4096 * 12 = ~48KB initial allocation.
29+
const parseResultPoolFieldCap = 4096
30+
31+
// parseResultPoolRowCap is the initial row capacity for pooled parseResult.
32+
// 1024 rows with rowInfo at 24 bytes = ~24KB initial allocation.
33+
const parseResultPoolRowCap = 1024
34+
2635
// parseResultPool provides reusable parseResult objects to reduce allocations
2736
var parseResultPool = sync.Pool{
2837
New: func() interface{} {
2938
return &parseResult{
30-
fields: make([]fieldInfo, 0, 512), // Smaller initial (6KB)
31-
rows: make([]rowInfo, 0, 128), // Smaller initial (1.5KB)
39+
fields: make([]fieldInfo, 0, parseResultPoolFieldCap),
40+
rows: make([]rowInfo, 0, parseResultPoolRowCap),
3241
}
3342
},
3443
}

quote.go

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -158,14 +158,17 @@ func unescapeDoubleQuotesScalar(s string) string {
158158

159159
// unescapeDoubleQuotesSIMD uses SIMD to find double quotes and unescape them.
160160
func unescapeDoubleQuotesSIMD(s string) string {
161+
if len(s) < simdChunkSize {
162+
return unescapeDoubleQuotesScalar(s)
163+
}
161164
data := unsafe.Slice(unsafe.StringData(s), len(s))
162-
quoteCmp := archsimd.BroadcastInt8x32('"')
165+
quoteCmp := archsimd.BroadcastInt8x64('"')
163166
var result []byte
164167
lastWritten := 0
165168
skipNextQuote := false
166169
i := 0
167-
for i+32 <= len(data) {
168-
chunk := archsimd.LoadInt8x32((*[32]int8)(unsafe.Pointer(&data[i])))
170+
for i+simdChunkSize <= len(data) {
171+
chunk := archsimd.LoadInt8x64((*[simdChunkSize]int8)(unsafe.Pointer(&data[i])))
169172
mask := chunk.Equal(quoteCmp).ToBits()
170173

171174
if skipNextQuote {
@@ -177,7 +180,7 @@ func unescapeDoubleQuotesSIMD(s string) string {
177180

178181
if mask != 0 {
179182
for mask != 0 {
180-
pos := bits.TrailingZeros32(mask)
183+
pos := bits.TrailingZeros64(mask)
181184
absPos := i + pos
182185

183186
if absPos+1 < len(data) && data[absPos+1] == '"' {
@@ -188,18 +191,18 @@ func unescapeDoubleQuotesSIMD(s string) string {
188191
result = append(result, s[lastWritten:absPos+1]...)
189192
lastWritten = absPos + 2 // Skip the second quote
190193
// Clear both bits if second quote is in same chunk
191-
mask &= ^(uint32(1) << pos)
192-
if pos+1 < 32 {
193-
mask &= ^(uint32(1) << (pos + 1))
194+
mask &^= uint64(1) << pos
195+
if pos+1 < simdChunkSize {
196+
mask &^= uint64(1) << (pos + 1)
194197
} else {
195198
skipNextQuote = true
196199
}
197200
continue
198201
}
199-
mask &= ^(uint32(1) << pos)
202+
mask &^= uint64(1) << pos
200203
}
201204
}
202-
i += 32
205+
i += simdChunkSize
203206
}
204207

205208
// Process remaining bytes

simd_scanner.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,10 @@ type scanResult struct {
105105
}
106106

107107
// scanResultPoolCapacity is the pre-allocated slice capacity for pooled scanResult objects.
108-
// 256 chunks = ~16KB input (256 * 64 bytes per chunk) - covers most typical CSV files.
109-
// Increased from 64 to reduce ensureUint64SliceCap reallocations observed in profiling.
110-
const scanResultPoolCapacity = 256
108+
// 2048 chunks = ~128KB input (2048 * 64 bytes per chunk) - covers most typical CSV files.
109+
// Increased from 256 to reduce ensureUint64SliceCap reallocations observed in profiling.
110+
// For 10K-row CSV (~600KB), this covers ~13% with initial capacity, reducing reallocs.
111+
const scanResultPoolCapacity = 2048
111112

112113
// scanResultPool provides reusable scanResult objects to reduce allocations.
113114
var scanResultPool = sync.Pool{
@@ -153,14 +154,20 @@ func releaseScanResult(sr *scanResult) {
153154

154155
// ensureUint64SliceCap ensures slice has at least required length with 2x growth.
155156
// Returns the slice with length set to required.
157+
// Note: This creates a new slice if capacity is insufficient; the old slice's
158+
// capacity is lost but this is acceptable since pooled objects start with
159+
// sufficient capacity (scanResultPoolCapacity) for most use cases.
156160
func ensureUint64SliceCap(s []uint64, required int) []uint64 {
157161
if cap(s) >= required {
158162
return s[:required]
159163
}
164+
// Use max of 2x growth or required capacity
160165
newCap := cap(s) * 2
161166
if newCap < required {
162167
newCap = required
163168
}
169+
// Add 25% headroom to reduce future reallocations
170+
newCap += newCap / 4
164171
return make([]uint64, required, newCap)
165172
}
166173

@@ -172,10 +179,13 @@ func ensureBoolSliceCap(s []bool, required int) []bool {
172179
clear(s)
173180
return s
174181
}
182+
// Use max of 2x growth or required capacity
175183
newCap := cap(s) * 2
176184
if newCap < required {
177185
newCap = required
178186
}
187+
// Add 25% headroom to reduce future reallocations
188+
newCap += newCap / 4
179189
return make([]bool, required, newCap)
180190
}
181191

0 commit comments

Comments
 (0)