Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions benchmarks/NPOI.Benchmarks/XSSFRowCellNumBenchmark.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using BenchmarkDotNet.Attributes;
using NPOI.SS.UserModel;
using NPOI.XSSF.UserModel;

namespace NPOI.Benchmarks;

[ShortRunJob]
[MemoryDiagnoser]
public class XSSFRowCellNumBenchmark
{
private XSSFWorkbook _workbook;
private IRow _sparseRow;
private IRow _denseRow;

[GlobalSetup]
public void Setup()
{
_workbook = new XSSFWorkbook();
var sheet = _workbook.CreateSheet("test");

// Sparse row: cells at widely separated columns
_sparseRow = sheet.CreateRow(0);
_sparseRow.CreateCell(0).SetCellValue("first");
_sparseRow.CreateCell(500).SetCellValue("mid");
_sparseRow.CreateCell(1000).SetCellValue("last");

// Dense row: 200 contiguous cells (typical spreadsheet)
_denseRow = sheet.CreateRow(1);
for (int i = 0; i < 200; i++)
{
_denseRow.CreateCell(i).SetCellValue(i);
}
}

[Benchmark]
public int SparseRow_FirstLastCellNum_10000x()
{
// Simulates tight loop pattern: for (j = row.FirstCellNum; j <= row.LastCellNum; j++)
int sum = 0;
for (int i = 0; i < 10_000; i++)
{
sum += _sparseRow.FirstCellNum + _sparseRow.LastCellNum;
}
return sum;
}

[Benchmark]
public int DenseRow_FirstLastCellNum_10000x()
{
int sum = 0;
for (int i = 0; i < 10_000; i++)
{
sum += _denseRow.FirstCellNum + _denseRow.LastCellNum;
}
return sum;
}

[Benchmark]
public int DenseRow_IterateCellRange()
{
// Pattern seen in copy/shift operations: iterate FirstCellNum..LastCellNum
int count = 0;
for (int j = _denseRow.FirstCellNum; j < _denseRow.LastCellNum; j++)
{
ICell cell = _denseRow.GetCell(j);
if (cell != null) count++;
}
return count;
}

[GlobalCleanup]
public void Cleanup()
{
_workbook?.Dispose();
}
}
75 changes: 72 additions & 3 deletions ooxml/XSSF/UserModel/XSSFRow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,18 @@ public class XSSFRow : IRow, IComparable<XSSFRow>
private readonly XSSFSheet _sheet;

private readonly StylesTable _stylesSource;

/// <summary>
/// Cached first (minimum) column index, or -1 if unknown/dirty.
/// Avoids O(n) LINQ Min() scan on every FirstCellNum access.
/// </summary>
private int _cachedFirstCellNum = -1;

/// <summary>
/// Cached last (maximum) column index, or -1 if unknown/dirty.
/// Avoids O(n) LINQ Max() scan on every LastCellNum access.
/// </summary>
private int _cachedLastCellNum = -1;
#endregion

#region Public properties
Expand All @@ -79,7 +91,12 @@ public short FirstCellNum
{
get
{
return (short)(_cells.Count == 0 ? -1 : GetFirstKey());
if (_cells.Count == 0) return -1;
if (_cachedFirstCellNum < 0)
{
_cachedFirstCellNum = GetFirstKey();
}
return (short)_cachedFirstCellNum;
}
}

Expand All @@ -94,7 +111,12 @@ public short LastCellNum
{
get
{
return (short)(_cells.Count == 0 ? -1 : (GetLastKey() + 1));
if (_cells.Count == 0) return -1;
if (_cachedLastCellNum < 0)
{
_cachedLastCellNum = GetLastKey();
}
return (short)(_cachedLastCellNum + 1);
}
}

Expand Down Expand Up @@ -284,6 +306,7 @@ public XSSFRow(CT_Row row, XSSFSheet sheet)
{
XSSFCell cell = new XSSFCell(this, c);
_cells.Add(cell.ColumnIndex, cell);
UpdateCacheOnAdd(cell.ColumnIndex);
sheet.OnReadCell(cell);
}
}
Expand Down Expand Up @@ -355,6 +378,7 @@ public ICell CreateCell(int columnIndex, CellType type)
}

_cells[columnIndex] = xcell;
UpdateCacheOnAdd(columnIndex);
return xcell;
}

Expand Down Expand Up @@ -421,7 +445,9 @@ public void RemoveCell(ICell cell)
((XSSFWorkbook)_sheet.Workbook).OnDeleteFormula(xcell);
}

_cells.Remove(cell.ColumnIndex);
int removedIndex = cell.ColumnIndex;
_cells.Remove(removedIndex);
InvalidateCacheOnRemove(removedIndex);
}

/// <summary>
Expand Down Expand Up @@ -625,6 +651,10 @@ internal void RebuildCells()

// Sort CT_Cols by index asc.
_row.c.Sort((col1, col2) => col1.r.CompareTo(col2.r));

// Cache is invalid after rebuild — keys may have changed
_cachedFirstCellNum = -1;
_cachedLastCellNum = -1;
}
#endregion

Expand Down Expand Up @@ -805,6 +835,45 @@ private int GetLastKey()
{
return _cells.Keys.Max();
}

/// <summary>
/// Update cached min/max on cell addition. O(1) — just compare with current bounds.
/// </summary>
private void UpdateCacheOnAdd(int columnIndex)
{
if (_cachedFirstCellNum < 0 || columnIndex < _cachedFirstCellNum)
{
_cachedFirstCellNum = columnIndex;
}
if (_cachedLastCellNum < 0 || columnIndex > _cachedLastCellNum)
{
_cachedLastCellNum = columnIndex;
}
}

/// <summary>
/// Invalidate cached min/max when a cell at a boundary is removed.
/// Only forces re-scan when the removed cell was at an edge.
/// </summary>
private void InvalidateCacheOnRemove(int removedIndex)
{
if (_cells.Count == 0)
{
_cachedFirstCellNum = -1;
_cachedLastCellNum = -1;
}
else
{
if (removedIndex == _cachedFirstCellNum)
{
_cachedFirstCellNum = -1; // will re-scan on next access
}
if (removedIndex == _cachedLastCellNum)
{
_cachedLastCellNum = -1; // will re-scan on next access
}
}
}
#endregion
}
}
Loading
Loading