技術文章

HotXLS 的列塊儲存格儲存與串流 XLSX 儲存

HotXLS 把工作表儲存格存放在精簡的 256 列區塊裡,透過惰性的區間疊加來解析列、欄與矩形格式,而不是建立儲存格物件,並在儲存時把每一列直接串流進套件的 deflate 串流。這三項改變合起來,決定了一份大型活頁簿的記憶體輪廓:尖峰用量取決於最大單一列,而非完整工作表 XML 的大小

這件事之所以重要,是因為一種每位試算表開發者遲早都會遇上的形狀。使用者把一整欄格式化——一次點擊、一百萬個儲存格——而質樸的物件模型會配置一百萬個儲存格物件來容納一個數字格式索引來回應。磁碟上的檔案保持嬌小,因為 XLSX 格式把這件事表達為單一 <col> 項目。行程可完全不保持嬌小

為什麼格式化一欄比填滿一欄更耗記憶體?

因為格式化沒有資料來為物件背書。一個帶有值的儲存格總得存在於某處。一個空白但有樣式的儲存格,只是為了承載一個樣式索引而存在,而具現化數百萬個這種儲存格,正是 Delphi 試算表應用程式在一份 Excel 能瞬間開啟的檔案上耗盡位址空間的典型方式

區間樣式疊加移除了這個需要。一條列、欄或矩形格式化指令,會以一個範圍加上它所貢獻的樣式部分的形式儲存一次,並在該範圍內某個儲存格實際被存取時惰性解析。疊加能在結構性編輯中存活——在一個已格式化區塊內插入一列,會移動區間而非重建它——而且會以精簡的欄、列與純樣式儲存格項目往返,這正是 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 列為單位的區塊存放,帶有穩定的公開 handle,以列為主序序列化,所以寫入一份工作表時,是以它將發出位元組的順序走訪記憶體,而不是在堆積裡追逐指標。穩定的 handle 對 API 面很重要:呼叫端持有的 handle,在區塊版面進行的內部重整之間保持有效,而這正是讓精簡表示成為一項實作細節、而非破壞性變更的原因

樣式池壓實與之並行。每次儲存前,沒有任何儲存格參照的字型、填滿、框線、數字格式、對齊與保護都會被丟棄。長壽的活頁簿累積未參照的樣式記錄,就像長壽的文件累積未使用的樣式一樣,而一份被使用者編輯了一小時的活頁簿,可以帶著數百筆這類記錄,進入一份沒有人會去讀它的檔案

列串流儲存,以及它不適用的時候

StreamingWrite 啟用時(這是預設值),每一列工作表會直接寫入套件的 deflate 串流。這個旗標關閉的替代方案,會先建構完整的工作表 XML 再壓縮,所以尖峰記憶體隨整份工作表縮放。串流讓它隨單一列縮放

共用字串與輔助零件透過同一條紀律跟進:一個可重用的 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 的情況,而預設為它付費,是為大多數應用程式永遠不會碰到的情況付費

如何判斷疊加是否真的被使用

看儲存格計數,而不是記憶體圖。如果在套用廣泛格式之後,工作表回報的實體儲存格數量合理,疊加就在盡它的本分。如果計數跳增了格式化範圍的大小,那就是程式碼路徑裡有什麼東西具現化了儲存格——通常是一個讀取範圍內每個儲存格來檢查其樣式的迴圈,這會一次一個儲存格地強制解析,毀掉整個安排

當你需要某個儲存格的有效格式時才解析樣式。不要為一百萬個儲存格解析樣式,只為了發現那一欄帶有數字格式;去問疊加。同樣的規則適用於寫入:把值指派給有值的儲存格,讓格式化保持為一個區間

剩餘的記憶體去了哪裡

一旦儲存格與樣式都精簡了,大型活頁簿上下一個最大的消耗者,是共用字串表以及檔案帶有的任何附屬零件——樞紐快取、繪圖、來自物件模型未建模之零件的保留 XML。它們各有各的策略,而誠實的答案是:沒有任何單一設定能一次解決全部

如果你的瓶頸在開啟而非儲存,選擇性載入就是那根槓桿:僅中繼資料與選擇性工作表載入的逐步解說,涵蓋了如何讀取活頁簿而不為你不會碰的工作表付費。對於非常大檔案的讀取路徑產出,請見 並行 XLSX 剖析與記憶體配置器的筆記;而對於完全不需要物件模型的純輸出工作負載,伺服器批次工作的串流寫入通常會比這裡的任何調校更合適

HotXLS 從 Delphi 與 C++Builder 的原生程式碼讀寫 XLS 與 XLSX,不需要安裝 Excel、也不需要 OLE 自動化,而這正是讓這些記憶體特性一開始就得以觀察與控制的基礎——支援的格式與 RAD Studio 版本請見 HotXLS 試算表元件頁