Skip to content

Commit b35eb7b

Browse files
committed
perf: optimize Rows.Columns with cell reuse and pre-allocation
- Add xlsxC.reset() method to clear cell struct for reuse - Add cellBuf field to Rows struct for per-cell allocation avoidance - Add numCols field to learn column count for slice pre-allocation - Use colRefToIndex fast path when FastReadMode has preloaded SST - Skip sharedStringsReader call when fastSSTLoaded is true These optimizations reduce allocations in the traditional Rows.Columns() path, complementing the FastRows raw parser for read-heavy workloads.
1 parent 69b1fbd commit b35eb7b

3 files changed

Lines changed: 78 additions & 7 deletions

File tree

rows.go

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ type Rows struct {
9494
decoder *xml.Decoder
9595
token xml.Token
9696
curRowOpts, seekRowOpts RowOpts
97+
// Reusable cell struct to avoid per-cell allocation
98+
cellBuf xlsxC
99+
// Learned column count for pre-allocation
100+
numCols int
97101
}
98102

99103
// Next will return true if it finds the next row element.
@@ -155,11 +159,18 @@ func (rows *Rows) Columns(opts ...Options) ([]string, error) {
155159
if rows.curRow > rows.seekRow {
156160
return nil, nil
157161
}
162+
// Pre-allocate cells slice based on learned column count
158163
var rowIterator rowXMLIterator
164+
if rows.numCols > 0 {
165+
rowIterator.cells = make([]string, 0, rows.numCols)
166+
}
159167
var token xml.Token
160168
rows.rawCellValue = rows.f.getOptions(opts...).RawCellValue
161-
if rows.sst, rowIterator.err = rows.f.sharedStringsReader(); rowIterator.err != nil {
162-
return rowIterator.cells, rowIterator.err
169+
// Fast path: skip sharedStringsReader if FastReadMode already loaded SST
170+
if !rows.f.fastSSTLoaded {
171+
if rows.sst, rowIterator.err = rows.f.sharedStringsReader(); rowIterator.err != nil {
172+
return rowIterator.cells, rowIterator.err
173+
}
163174
}
164175
for {
165176
if rows.token != nil {
@@ -181,6 +192,10 @@ func (rows *Rows) Columns(opts ...Options) ([]string, error) {
181192
rows.seekRowOpts = extractRowOpts(xmlElement.Attr)
182193
if rows.curRow > rows.seekRow {
183194
rows.token = nil
195+
// Learn column count for next row's pre-allocation
196+
if rows.numCols == 0 && len(rowIterator.cells) > 0 {
197+
rows.numCols = len(rowIterator.cells)
198+
}
184199
return rowIterator.cells, rowIterator.err
185200
}
186201
}
@@ -191,6 +206,9 @@ func (rows *Rows) Columns(opts ...Options) ([]string, error) {
191206
rows.token = nil
192207
case xml.EndElement:
193208
if xmlElement.Name.Local == "sheetData" {
209+
if rows.numCols == 0 && len(rowIterator.cells) > 0 {
210+
rows.numCols = len(rowIterator.cells)
211+
}
194212
return rowIterator.cells, rowIterator.err
195213
}
196214
}
@@ -250,15 +268,20 @@ type rowXMLIterator struct {
250268
func (rows *Rows) rowXMLHandler(rowIterator *rowXMLIterator, xmlElement *xml.StartElement, raw bool) {
251269
if rowIterator.inElement == "c" {
252270
rowIterator.cellCol++
253-
colCell := xlsxC{}
254-
_ = colCell.cellXMLHandler(rows.decoder, xmlElement)
255-
if colCell.R != "" {
256-
if rowIterator.cellCol, _, rowIterator.err = CellNameToCoordinates(colCell.R); rowIterator.err != nil {
271+
// Reuse cell buffer to avoid per-cell allocation
272+
cell := &rows.cellBuf
273+
cell.reset()
274+
_ = cell.cellXMLHandler(rows.decoder, xmlElement)
275+
if cell.R != "" {
276+
// Use fast inline column parser when FastReadMode is enabled
277+
if rows.f.fastSSTLoaded {
278+
rowIterator.cellCol = colRefToIndex(cell.R)
279+
} else if rowIterator.cellCol, _, rowIterator.err = CellNameToCoordinates(cell.R); rowIterator.err != nil {
257280
return
258281
}
259282
}
260283
blank := rowIterator.cellCol - len(rowIterator.cells)
261-
if val, _ := colCell.getValueFrom(rows.f, rows.sst, raw); val != "" || colCell.F != nil {
284+
if val, _ := cell.getValueFrom(rows.f, rows.sst, raw); val != "" || cell.F != nil {
262285
rowIterator.cells = append(appendSpace(blank, rowIterator.cells), val)
263286
}
264287
}

rows_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2575,3 +2575,35 @@ func TestRowsFastPeekError(t *testing.T) {
25752575
fr2.Close()
25762576
}
25772577
}
2578+
2579+
func TestRowsColumnsWithFastSSTLoaded(t *testing.T) {
2580+
// Exercise the colRefToIndex fast path in rowXMLHandler when fastSSTLoaded=true
2581+
f := NewFile()
2582+
defer f.Close()
2583+
f.SetCellValue("Sheet1", "A1", "hello")
2584+
f.SetCellValue("Sheet1", "B1", "world")
2585+
f.SetCellValue("Sheet1", "A2", "foo")
2586+
2587+
buf, err := f.WriteToBuffer()
2588+
require.NoError(t, err)
2589+
2590+
f2, err := OpenReader(buf, Options{FastReadMode: true})
2591+
require.NoError(t, err)
2592+
defer f2.Close()
2593+
2594+
rows, err := f2.Rows("Sheet1")
2595+
require.NoError(t, err)
2596+
defer rows.Close()
2597+
2598+
// First row — triggers colRefToIndex fast path
2599+
assert.True(t, rows.Next())
2600+
cols, err := rows.Columns()
2601+
assert.NoError(t, err)
2602+
assert.Equal(t, []string{"hello", "world"}, cols)
2603+
2604+
// Second row — exercises numCols pre-allocation
2605+
assert.True(t, rows.Next())
2606+
cols, err = rows.Columns()
2607+
assert.NoError(t, err)
2608+
assert.Equal(t, []string{"foo"}, cols)
2609+
}

xmlWorksheet.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,22 @@ type xlsxC struct {
515515
f string
516516
}
517517

518+
// reset clears the cell struct for reuse, avoiding allocation of a new struct.
519+
func (c *xlsxC) reset() {
520+
c.XMLName = xml.Name{}
521+
c.XMLSpace = xml.Attr{}
522+
c.R = ""
523+
c.S = 0
524+
c.T = ""
525+
c.Cm = nil
526+
c.Vm = nil
527+
c.Ph = nil
528+
c.F = nil
529+
c.V = ""
530+
c.IS = nil
531+
c.f = ""
532+
}
533+
518534
// xlsxF represents a formula for the cell. The formula expression is
519535
// contained in the character node of this element.
520536
type xlsxF struct {

0 commit comments

Comments
 (0)