技術記事

HotXLSの行ブロックセルストレージとストリーミングXLSX保存

HotXLSはワークシートのセルをコンパクトな256行ブロックで保存し、行、列、矩形の書式設定を、セルオブジェクトを生成する代わりに遅延区間オーバーレイで解決し、保存時には各行を直接パッケージのdeflateストリームに流し込む。この3つの変更が合わさって、大きなブックのメモリプロファイルを決定する。ピーク使用量はワークシートXML全体のサイズではなく、最大の単一行に追従する

これが意味を持つ理由は、すべてのスプレッドシート開発者がいずれ出会う形だ。ユーザーが列全体を書式設定する。1クリック、100万セルだ。そして素朴なオブジェクトモデルは、1つの数字書式インデックスを保持するために100万のセルオブジェクトを割り当てて応答する。ディスク上のファイルは、XLSXフォーマットがそれを1つの<col>エントリとして表現するので小さく留まる。プロセスはまったく小さく留まらない

なぜ列の書式設定はデータ埋めよりメモリを食うのか

書式設定には、オブジェクトを正当化するデータがないからだ。値を持つセルはどこかに存在しなければならない。空だがスタイル付きのセルは、スタイルインデックスを運ぶためだけに存在し、これを数百万人規模で実体化するのが、Delphiスプレッドシートアプリケーションが、Excelなら即座に開くファイル上でアドレス空間を使い果たす典型的な方法だ

区間スタイルオーバーレイがその必要性を取り除く。行、列、矩形の書式設定指示は、範囲とそれが寄与するスタイル部分として1回保存され、その範囲のセルが実際にアクセスされたときに遅延解決される。オーバーレイは構造的編集を生き残る。書式設定されたブロックの中に行を挿入すると、区間はリビルドされるのではなく移動する。そしてコンパクトな列、行、スタイルのみのセルエントリとしてラウンドトリップする。これはExcelが書く形そのものだ

var
  Sheet: TXLSXWorksheet;
  State: TXLSXCellStyleState;
begin
  Sheet := Workbook.Sheets[1];
  // Style indexes come from the workbook style pools, e.g. from a cell
  // you have already formatted the way you want the range to look
  State := BuildStateFromTemplateCell(Sheet.Cells.Item[1, 1]);
  // Format columns B..D without creating a single empty cell object
  Sheet.StyleOverlays.Add(1, 2, MaxRowIndex, 4,
    [xfpNumberFormat, xfpAlignment], State);
end;

TXLSXFormatPartsは、オーバーレイが何を寄与するかを決める集合だ。xfpFontxfpFillxfpBorderxfpNumberFormatxfpAlignmentxfpProtection。意図する部分だけを名指すことで、オーバーレイがまともに階層化できる。数字書式を供給する列オーバーレイは、塗りを供給する行オーバーレイと喧嘩しない。どちらも相手の部分を主張しないからだ

256行ブロックが与えるもの

局所性だ。セルは256行のブロックで、安定した公開ハンドルとともに保持され、行優先でシリアライズされる。だからワークシートを書くとき、ヒープ上のポインタを追いかけるのではなく、バイトを吐き出す順序でメモリを歩く。安定したハンドルはAPI面で重要だ。呼び出し側が保持するハンドルは、ブロックレイアウトが行う内部再編成をまたいで有効なままである。これこそ、コンパクトな表現を破壊的変更ではなく実装の詳細にするものだ

スタイルプールのコンパクションがその隣で走る。保存のたびに、どのセルも参照しないフォント、塗り、罫線、数字書式、配置、保護が落とされる。長生きするブックは、長生きするドキュメントが未使用スタイルを蓄積するのと同じように、参照されないスタイルレコードを蓄積する。ユーザーに1時間編集されたブックは、誰も読まないファイルに向かう数百のスタイルレコードを運び得る

行ストリーミング保存と、それが適用されないとき

StreamingWriteを有効にすると——これがデフォルトだ——各ワークシート行は直接パッケージのdeflateストリームに書かれる。このフラグがオフにする対抗馬は、完全なワークシートXMLを最初に構築し、その後に圧縮する。だからピークメモリはシート全体にスケールする。ストリーミングはそれを1行にスケールさせる

共有文字列と補助パートも、エントリを1度に1つずつ吐き出す1つの再利用可能なUTF-8シリアライザを通じて同じ規律に従う。これによりピークメモリはパート全体ではなく最大の単一エントリで抑えられる。これは共有文字列テーブルとピボットレコードをカバーし、広い分析ブックでは個々のワークシートより大きいことがよくある

var
  Workbook: TXLSXWorkbook;
begin
  Workbook := TXLSXWorkbook.Create(nil);
  try
    Workbook.Open('ledger-2026.xlsx');
    // StreamingWrite defaults to True; turn it off only when a downstream
    // step requires the whole worksheet XML to exist before compression
    Workbook.StreamingWrite := True;
    Workbook.SaveAs('ledger-2026-out.xlsx');
  finally
    Workbook.Free;
  end;
end;

具体的な理由がない限り、オンのままにすること。非ストリーミングパスは、パイプラインのほかの何かが組み立て済みXMLを必要とするケースのために存在し、デフォルトでそのコストを払うのは、ほとんどのアプリケーションが決して当たらないケースに払うことだ

オーバーレイが実際に使われているかをどうやって知るか

メモリグラフではなく、セル数を見ること。広範な書式を適用した後に、ワークシートが妥当な数の物理セルを報告すれば、オーバーレイは仕事をしている。カウントが書式設定された範囲のサイズだけ跳ね上がれば、コードパスの何かがセルを実体化したのだ。通常は、範囲のすべてのセルを読んでスタイルをチェックするループで、これがセルを1つずつ強制的に解決し、構成全体を台無しにする

1つのセルの有効な書式が必要なときにスタイルを解決すること。列が数字書式を持つことを知るために100万セルのスタイルを解決しないこと。オーバーレイに聞くこと。書き込みにも同じルールが当てはまる。値を持つセルに値を割り当て、書式設定は区間のままにしておく

残るメモリはどこへ行くのか

セルとスタイルがコンパクトになったら、次に大きい消費者は、大きなブック上では共有文字列テーブルと、ファイルが運ぶ補助パート——ピボットキャッシュ、図、オブジェクトモデルがモデル化しないパートの保存XML——になる。これらにはそれぞれ独自の戦略があり、正直な答えは、単一の設定ですべてを一度に解決するものはない、ということだ

ボトルネックが保存ではなくオープンなら、選択的読み込みがレバーになる。メタデータのみと選択的ワークシート読み込みの解説が、触れないシートのコストを払わずにブックを読む方法を扱う。非常に大きなファイルの読み取りパスのスループットについては、並列XLSXパースとメモリアロケータのノートを参照のこと。そしてオブジェクトモデルをまったく必要としない出力専用ワークロードには、サーバーバッチジョブ向けストリーミング書き込みが、ここでのチューニングの量よりもふさわしいことが多い

HotXLSはネイティブのDelphiとC++Builderコードから、ExcelのインストールもOLEオートメーションもなしにXLSとXLSXを読み書きする。これこそが、こうしたメモリ特性をそもそも観察可能で制御可能にするものだ。HotXLSスプレッドシートコンポーネントページにサポートされるフォーマットとRAD Studioバージョンの一覧がある