104 万行 × 16,384 列のシートを作っても、値を書くまでメモリはほとんど使いません。 V5 は実在するセルにだけコストを払う設計になっており、シートの寸法ではなく 「実際に何セル書いたか」「画面に何セル映っているか」で速度が決まります。
このページは、その特性を前提にコードを書くための指針です。
前提にしてよいこと
| # | 特性 |
|---|---|
| 1 | 空セルはコストを持たない。 フル寸法のシートを 100 枚作っても、空ならメモリはほぼゼロ |
| 2 | セルあたり約 16〜23 バイト。 値は 16 バイトの構造体で、boxing は発生しない |
| 3 | 同じ内容の書式は 1 つに統合される。 何万セルに同じ書式を当てても実体は 1 つ |
| 4 | 同じ文字列は 1 つに統合される。 繰り返し現れる文字列はシート内で共有される |
| 5 | 行・列の設定は差分だけを持つ。 高さや幅を変えていない行はエントリを持たない |
| 6 | 走査は実在するセルの数に比例する。 空セルは訪問されない |
| 7 | 描画と操作は可視範囲に比例する。 シートの寸法には依存しない |
これらは仕様として保証します。前提にしてコードを書いて構いません。
走査は使用範囲に絞る
一番多い性能問題は、0 から RowCount までを素朴に回してしまうことです。
空セルの読み出し自体は安価ですが、104 万回という回数が現実的ではありません。
using unvell.ReoGrid.Core;
using unvell.ReoGrid.Core.Style;
// 良い例 — 使用範囲にクランプしてから回す
if (ws.TryGetUsedRange(out var used))
{
for (int r = used.Row; r <= used.EndRow; r++)
for (int c = used.Col; c <= used.EndCol; c++)
_ = ws.GetValue(r, c);
}
TryGetUsedRange は値・数式・書式のどれかが入っている範囲を返します。
シートが空なら false です。
疎なデータは ReadWindow で走査する
使用範囲は広いが中身が飛び飛び、という形(1 万行おきに 1 行だけ埋まっているなど)では、
ReadWindow が明確に速くなります。実在するセルだけがコールバックに渡り、
空セルでは呼ばれません。
// さらに良い例 — 実在するセルだけが訪問される(空セルは呼ばれない)
ws.ReadWindow(0, 0, ws.RowCount, ws.ColumnCount, (row, col, value, styleId) =>
{
_ = value;
});
描画・XLSX 出力・PDF 出力はいずれもこの形を使っています。集計やエクスポートを 自分で書く場合も、まずこれを検討してください。
行・列全体の書式は行・列に設定する
// 良い例 — 列の既定スタイルは 1 エントリで済む
ws.SetColumnStyle(2, new StyleRecord { TextAlign = HAlign.Right });
同じことをセルのループで書くと、その列のセル数ぶんのエントリが実体化します。 特性 1(空セルはコストを持たない)が失われる、最も多い原因です。
見た目は同じでも、メモリ使用量と処理時間はまったく違います。詳しくは スタイルの継承 を参照してください。
書式は使い回さなくてよい
同じ内容の書式は自動的に 1 つに統合されるため、StyleRecord を変数に取っておく
必要はありません。ループの中で毎回 new しても実体は共有されます。
// 同じ内容のスタイルは 1 つに統合される(record の値等価性でインターン)
for (int r = 0; r < 10_000; r++)
ws.SetCellStyle(r, 0, new StyleRecord { Bold = true });
int distinct = ws.DistinctStyleCount; // 増えるのは 1 つだけ
DistinctStyleCount は診断に使えます。想定より大きい場合は、セルごとに微妙に違う
書式(1 セルだけフォントサイズが 0.5 違う、など)が生まれていないか確認してください。
大量のデータを流し込む
数万行を書き込むときは、次の順序が最も速くなります。
- 行・列のサイズと書式を先に決める(
ws.Rows.SetSize/ws.Columns.SetSize/SetRowStyle/SetColumnStyle) - 値を書く(
SetNumber/SetTextが最も軽い。SetObjectValueは型判定のぶん遅い) - 数式を書く
SetFormula はそのセルと、それに依存するセルをその場で再計算します。数式どうしが
依存し合う表を流し込むときは、依存される側(合計の元になるセル)から先に書くと
再計算の回数が減ります。順序を気にしたくない場合は、値だけ先に全部書いてから数式を書き、
最後に ws.Recalculate() を 1 回呼んでください。
RowDataSource を設定すると、行を表示されたときに初めて用意することもできます。
数百万行のデータベース結果をシートに見せる場合に使います。
ws.RowDataSource = new MyRowSource(); // IRowDataSource を実装する
ws.EnsureRowsLoaded(0, 99); // 明示的に先読みしたいとき
コントロールは表示直前に可視範囲ぶんの EnsureRowsLoaded を自動的に呼びます。
既に用意済みの行は読み飛ばされるため、毎フレーム呼ばれても問題ありません。
巨大な XLSX を開く場合は、ファイル側も部分読み込みにできます
(XLSX 入出力 の OpenVirtual)。
特性を壊す書き方
| やってはいけないこと | 何が起きるか | 代わりに |
|---|---|---|
for (int r = 0; r < ws.RowCount; r++) | 104 万回の反復 | TryGetUsedRange でクランプ / ReadWindow |
| 列全体にセル単位で書式を設定 | 列のセル数ぶん実体化 | SetColumnStyle |
| 空セルを「初期化」するために値を書く | 特性 1 を失う | 書かない。空のまま扱う |
| セルごとに少しずつ違う書式を作る | 統合が効かない | 書式のバリエーションを絞る |
ループの中で Recalculate() | 毎回シート全体を再計算 | 書き終えてから 1 回だけ |
計測する
メモリと割り当ての実測にはベンチマークプロジェクトが使えます。
dotnet run -c Release --project ReoGrid.Core.Bench
アプリ側で当たりを付けるなら、ws.DistinctStyleCount と ws.TryGetUsedRange の
返す範囲を見るのが手軽です。想定より大きければ、上の表のどれかを踏んでいます。