Technischer Artikel

Row-Block-Speicherung und gestreamtes XLSX-Speichern in HotXLS

HotXLS speichert Arbeitsblatt-Zellen in kompakten 256-Zeilen-Blöcken, löst Zeilen-, Spalten- und Rechteck-Formatierung über lazy Intervall-Overlays statt Zell-Objekte anzulegen und streamt beim Speichern jede Zeile direkt in den Deflate-Stream des Pakets. Zusammen entscheiden diese drei Änderungen das Speicherprofil einer großen Arbeitsmappe: Die Spitzenauslastung folgt der größten einzelnen Zeile statt der Größe des vollständigen Arbeitsblatt-XML

Der Grund, warum das zählt, ist eine Form, der jeder Tabellenkalkulations-Entwickler irgendwann begegnet. Ein Nutzer formatiert eine gesamte Spalte — ein Klick, eine Million Zellen — und ein naives Objekt-Modell antwortet, indem es eine Million Zell-Objekte allokriert, um einen einzigen Zahlenformat-Index zu halten. Die Datei auf der Platte bleibt winzig, weil das XLSX-Format das als einen einzigen <col>-Eintrag ausdrückt. Der Prozess bleibt es überhaupt nicht

Warum kostet das Formatieren einer Spalte mehr Speicher als das Füllen?

Weil Formatierung keine Daten hat, die das Objekt rechtfertigen. Eine Zelle mit einem Wert muss irgendwo existieren. Eine Zelle, die leer, aber formatiert ist, existiert nur, um einen Style-Index zu tragen, und Millionen davon zu materialisieren ist der klassische Weg, auf dem eine Delphi-Tabellenkalkulations-Anwendung bei einer Datei, die Excel sofort öffnet, den Adressraum erschöpft

Intervall-Style-Overlays beseitigen das Erfordernis. Eine Zeilen-, Spalten- oder Rechteck-Formatierungs-Anweisung wird einmal als Bereich plus den Style-Teilen gespeichert, die sie beisteuert, und löst sich lazy auf, wenn auf eine Zelle in diesem Bereich tatsächlich zugegriffen wird. Overlays überleben strukturelle Bearbeitungen — eine Zeile innerhalb eines formatierten Blocks einzufügen verschiebt das Intervall statt es neu aufzubauen — und sie round-trippen als kompakte Spalten-, Zeilen- und nur-Style-Zell-Einträge, was genau so ist, wie Excel sie schreibt

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 ist die Menge, die entscheidet, was ein Overlay beisteuert: xfpFont, xfpFill, xfpBorder, xfpNumberFormat, xfpAlignment und xfpProtection. Nur die Teile zu benennen, die man meint, ist es, was Overlays vernünftig schichten lässt — ein Spalten-Overlay, das ein Zahlenformat beisteuert, gerät nicht in Konflikt mit einem Zeilen-Overlay, das eine Füllung beisteuert, weil keiner den Teil des anderen beansprucht

Was der 256-Zeilen-Block einem gibt

Lokalität. Zellen werden in Blöcken zu 256 Zeilen mit stabilen öffentlichen Handles gehalten, zeilen-serialisiert, sodass das Schreiben eines Arbeitsblatts den Speicher in der Reihenfolge durchwandert, in der es Bytes emittieren wird, statt Pointern über den Heap nachzujagen. Stabile Handles zählen für die API-Oberfläche: Ein Handle, das ein Aufrufer hält, bleibt über die interne Umorganisation gültig, die das Block-Layout durchführt, was die kompakte Repräsentation zu einem Implementierungs-Detail statt einem Breaking Change macht

Style-Pool-Verdichtung läuft daneben. Vor jedem Speichern werden Fonts, Füllungen, Rahmen, Zahlenformate, Ausrichtungen und Schutzarten, die keine Zelle referenziert, fallengelassen. Langlebige Arbeitsmappen häufen unreferenzierte Style-Datensätze an wie langlebige Dokumente ungenutzte Styles, und eine Arbeitsmappe, die ein Nutzer eine Stunde lang bearbeitet hat, kann Hunderte davon in eine Datei tragen, die niemand jemals lesen wird

Zeilen-gestreamtes Speichern, und wann es nicht gilt

Mit StreamingWrite aktiviert — die Standardeinstellung — wird jede Arbeitsblatt-Zeile direkt in den Deflate-Stream des Pakets geschrieben. Die Alternative, die das Flag ausschaltet, baut erst das vollständige Arbeitsblatt-XML auf und komprimiert es danach, sodass die Spitzenauslastung mit dem ganzen Blatt skaliert. Streaming lässt sie mit einer Zeile skalieren

Shared Strings und Hilfs-Parts folgen derselben Disziplin über einen wiederverwendbaren UTF-8-Serializer, der Einträge einzeln emittiert und die Spitzenauslastung durch den größten einzelnen Eintrag statt durch den ganzen Part begrenzt. Das deckt die Shared-String-Tabelle und Pivot-Datensätze ab, die in einer breiten analytischen Arbeitsmappe häufig größer sind als jedes einzelne Arbeitsblatt

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;

Eingeschaltet lassen, es sei denn, es gibt einen konkreten Grund dagegen. Der nicht-streamende Pfad existiert für die Fälle, in denen etwas anderes in der Pipeline das assemblierte XML braucht, und standardmäßig dafür zu zahlen heißt, für einen Fall zu zahlen, den die meisten Anwendungen nie treffen

Wie man feststellt, ob Overlays tatsächlich verwendet werden

Auf die Zellen-Anzahl achten, nicht auf den Speicher-Graphen. Meldet ein Arbeitsblatt nach breit angelegter Formatierung eine plausible Anzahl physischer Zellen, tun Overlays ihre Arbeit. Springt die Anzahl um die Größe des formatierten Bereichs, hat etwas im Code-Pfad die Zellen materialisiert — meist eine Schleife, die jede Zelle im Bereich liest, um ihren Style zu prüfen, was die Auflösung Zelle für Zelle erzwingt und die gesamte Anordnung zunichtemacht

Einen Style auflösen, wenn man das effektive Format einer einzigen Zelle braucht. Nicht einen Style für eine Million Zellen auflösen, um herauszufinden, dass die Spalte ein Zahlenformat hat; das Overlay fragen. Dasselbe gilt fürs Schreiben: Werte den Zellen zuweisen, die Werte haben, und die Formatierung ein Intervall bleiben lassen

Wohin der übrige Speicher geht

Sind Zellen und Styles kompakt, sind die nächstgrößten Verbraucher in einer großen Arbeitsmappe die Shared-String-Tabelle und welche Satelliten-Parts die Datei trägt — Pivot-Caches, Zeichnungen, erhaltenes XML aus Parts, die das Objekt-Modell nicht modelliert. Diese haben ihre eigenen Strategien, und die ehrliche Antwort lautet, dass keine einzelne Einstellung sie alle zugleich löst

Wenn der Engpass beim Öffnen statt beim Speichern liegt, ist selektives Laden der Hebel: die Erläuterung zum Metadata-Only- und selektiven Arbeitsblatt-Laden behandelt das Lesen einer Arbeitsmappe, ohne für die Blätter zu zahlen, die man nicht anfassen wird. Für Read-Path-Durchsatz auf sehr großen Dateien siehe die Hinweise zum parallelen XLSX-Parsing und der Speicher-Allokator, und für reine Output-Workloads, die gar kein Objekt-Modell brauchen, ist gestreamtes Schreiben für Server-Batch-Aufträge meist besser geeignet als jede Menge Tuning hier

HotXLS liest und schreibt XLS und XLSX aus nativem Delphi- und C++Builder-Code ohne Excel-Installation und ohne OLE-Automatisierung, was diese Speicher-Eigenschaften überhaupt erst beobachtbar und steuerbar macht — die HotXLS-Spreadsheet-Komponenten-Seite listet die unterstützten Formate und RAD-Studio-Versionen