Wanneer een export van 300.000 rijen zijn geheugenbudget doorbreekt, krijgt het rijaantal meestal de schuld. Het rijaantal is meestal onschuldig. De dure onderdelen van een grote werkmap zijn degene die als bijeffect ontstaan: een stijlpool die met één item per cel groeit omdat opmaak binnen de lus werd toegevoegd, werkblad-XML die op opslagmoment als één gigantische string wordt opgebouwd, een miljoen identieke formulebodies één voor één opgeslagen. HotXLS, losLabs native Delphi-bibliotheek voor XLS- en XLSX-bestanden, geeft je een specifieke hendel voor elk van deze kosten. Geen enkele staat standaard aan, want elke verandert een afweging, dus weten welke hendel bij welk symptoom past is de eigenlijke prestatievaardigheid
Waar een grote werkmap geheugen besteedt
Er zijn twee afzonderlijke geheugenregimes om over na te denken. Tijdens generatie groeit het in-memory celmodel met elke cel die je aanraakt: waarden, formaten en formules worden allemaal objecten of poolitems. Tijdens opslaan rendert het standaard XLSX-pad daarnaast de XML van elk werkblad naar een brede string voordat het die comprimeert in de zip-container, dus het piekgebruik is het model plus de geserialiseerde vorm van het grootste blad. Een taak die de bouwlus overleeft en dan binnen SaveAs sterft, raakt het tweede regime, niet het eerste, en de fix voor het ene doet niets voor het andere
Bestandsgrootte volgt een verwante regel: cellen zijn maar één bijdrager, naast stijlen, gedeelde strings, formules, afbeeldingen en opmerkingen. Een auditpas met ForEachCell en de per-blad-verzamelingsaantallen vertelt je welke resource een probleembestand daadwerkelijk domineert voordat je het verkeerde optimaliseert. Eén meetsubtiliteit: Sheet.Cells.Count aan de XLSX-kant meldt het aantal geïnstantieerde cellen in de sparse store, niet het oppervlak van het gebruikte bereik. Een blad waarvan de data een rechthoek van 1000 bij 50 beslaat met de helft van de cellen leeg, telt ruwweg 25.000, niet 50.000. Dat onderscheid is van belang wanneer je het "enorme" bestand van een klant vergelijkt met je eigen fixtures, want het oppervlak van het gebruikte bereik en de werkelijke celpopulatie kunnen een orde van grootte verschillen in schaars gevulde financiële lay-outs
StreamingWrite fixt het opslagpad, niet het bouwpad
TXLSXWorkbook.StreamingWrite := True instellen schakelt SaveAs over naar een streamende serializer die werkblad-XML rechtstreeks naar de zip-stream schrijft, waardoor de tussenliggende string per blad wegvalt. Standaard staat het op False voor gedragscompatibiliteit, en het aanzetten is een wijziging van één regel:
Book := TXLSXWorkbook.Create;
try
Sheet := Book.Sheets.Add('Bulk');
for R := 1 to 100000 do
begin
Sheet.Cells[R, 1].Value := R;
Sheet.Cells[R, 2].Value := 'Row ' + IntToStr(R);
Sheet.Cells[R, 3].Value := R * 1.5;
end;
Book.StreamingWrite := True; // blad-XML streamt naar de zip-container
Book.SaveAs('bulk.xlsx');
finally
Book.Free;
end;
Wees precies over wat dit oplevert: het celmodel dat de lus bouwt, neemt precies evenveel geheugen in beslag als voorheen. StreamingWrite vlakt de piek op opslagmoment af, wat het verschil is tussen een batchtaak die voltooit en één die faalt bij de 95%-markering. Als de bouwlus zelf het geheugen uitput, zijn de volgende twee hendels degene die je nodig hebt
Stijlpools: eenmaal toevoegen, index hergebruiken
XLSX-opmaak in HotXLS is poolgebaseerd: Book.Fonts.Add(...), Fills.AddSolid(...) en Borders.Add(...) geven een 0-gebaseerde poolindex terug waar cellen naar verwijzen. Fonts.Add aanroepen met identieke parameters binnen een lus wordt gededupliceerd, dus het verspilt tijd in plaats van ruimte. Alignments.Add gedraagt zich anders: het geeft een vers object terug per aanroep, dus per-cel-uitlijning aanmaken laat de pool lineair meegroeien met het rijaantal. Eén gewoonte dekt beide gevallen. Los elke poolindex eenmaal op, buiten de lus, en wijs indices binnen de lus toe
// til poollookups uit de hete lus
HeaderFont := Book.Fonts.Add('Calibri', 11, True, False); // 0-gebaseerde poolindex
for C := 1 to 24 do
Sheet.Cells[1, C].FontIndex := HeaderFont + 1; // cellen slaan 1-gebaseerd op; 0 = standaard
De + 1 is geen typfout, en hem vergeten is hier de klassieke symptoomveroorzakende bug: de pools geven 0-gebaseerde indices uit, terwijl de celzijde-eigenschappen 0 behandelen als "standaard", dus elke poolindex moet bij toewijzing met één worden opgeschoven. Doe het per ongeluk fout en je koppen renderen stilletjes in het standaardlettertype van de werkmap, een defect dat niemand opmerkt tot de brandingreview
Vervang per-cel-Variant-verkeer door rijcallbacks
Elke Sheet.Cells[R, C].Value := X omvat een cel-lookup-of-aanmaak plus een Variant-toewijzing. Bij een paar honderdduizend cellen wordt die overhead per toegang meetbaar in profielen. HotXLS biedt bulk-callback-API's op beide facades (ForEachCell en ForEachRow voor lezen, WriteCells en WriteRows voor schrijven) die de iteratie binnen de engine verplaatsen en je code hele rijen tegelijk geven:
procedure TLedgerExport.FillRow(Sender: TObject;
SheetIndex, Row, FirstCol, LastCol: Integer;
var Values: Variant; var Skip: Boolean; var Cancel: Boolean);
begin
if Row > FCount then
begin
Cancel := True; // stop het hele schrijven
Exit;
end;
Values := VarArrayOf([FRows[Row - 1].Account,
FRows[Row - 1].PostedOn,
FRows[Row - 1].Amount]);
end;
// één engine-aanroep in plaats van honderdduizenden eigenschapstoegangen
Sheet.WriteRows(1, 1, FCount, 3, FillRow);
De Skip-vlag van de callback laat een rij onaangeraakt zonder af te breken, en Cancel beëindigt de bewerking vroegtijdig, wat nuttig is wanneer de bron een reader is waarvan je de lengte gaandeweg ontdekt. Combineer WriteRows voor de bouw met StreamingWrite voor het opslaan en het generatiepad heeft geen overgebleven per-cel-hotspot meer
Leeshendels aan de XLS-facade
Grote legacy .xls-bestanden hebben hun eigen toolkit. _DisableGraphics := True vóór Open slaat het parsen van de tekenlaag helemaal over, wat het laden van werkmappen versnelt die jaren aan opgehoopte vormen en ingesloten afbeeldingen dragen. De beperking is hard: de tekenlaag ontbreekt dan in het model, dus opslaan van zo'n werkmap schrijft een bestand zonder zijn tekeningen. Bewaar deze vlag voor alleen-lezen analysetaken. SetTempDir herleidt de tijdelijke bestanden van de BIFF-schrijver, wat van belang is op servers waar de standaard tijdelijke locatie een quotum heeft of op trage opslag staat. UseSharedFormulas groepeert herhaalde formulebodies in shared-formula-records, wat bestanden verkleint waar een formulekolom zich zestigduizend rijen lang herhaalt
Leeslussen over XLS-data hebben een indexeringsvalkuil die het waard is te markeren, want die verdubbelt het werk wanneer defensief afgehandeld en verknoeit resultaten wanneer gemist: UsedRange meldt zijn FirstRow-, LastRow-, FirstCol- en LastCol-grenzen 0-gebaseerd, terwijl Cells.Item[Row, Col] 1-gebaseerd is. Een scan die het gebruikte bereik doorloopt, moet bij de celtoegang één optellen bij elke coördinaat, zoals in Cells.Item[Row + 1, Col + 1], anders leest hij een raster dat diagonaal één cel is verschoven, waarbij stilletjes de laatste rij en kolom wegvallen en een fantoom eerste wordt meegenomen. De ForEachCell-callback omzeilt de mismatch volledig, wat nog een reden is om hem te verkiezen voor scans van hele bladen
Bestanden onderzoeken voordat je ze laadt
De goedkoopste grote-werkmapbewerking is degene die je vermijdt. GetSheetNames op beide facades somt de werkbladen van een bestand op zonder celdata te laden. De XLSX-implementatie leest alleen het workbook-manifest binnen de zip en laat de workbook-instantie expliciet ongevuld, en de XLS-facade stopt met scannen bij de eerste substream-grens. Dat maakt het de juiste pre-flight-controle voor "op welk blad moet deze importtaak zich richten", en CanReadEncrypted beantwoordt "is dit een versleutelde container" vóór een gedoemde Open-poging
Names := TStringList.Create;
Book := TXLSXWorkbook.Create;
try
if Book.GetSheetNames('big-unknown.xlsx', Names) <= 0 then
raise Exception.Create('cannot enumerate sheets'); // bij mislukking wordt de lijst geleegd
// kies het doelblad, en bepaal dan of een volledige Open de moeite waard is
finally
Book.Free;
Names.Free;
end;
Let op de returncode-conventie: deze onderzoeksfuncties signaleren mislukking met waarden op of onder nul en legen de uitvoerlijst, dus test op <= 0 in plaats van te vergelijken met één specifieke succeswaarde
De aanpak afstemmen op de taak
Voor onbewaakte pipelines die achtereenvolgens veel grote bestanden genereren, maken twee gewoonten meer het plaatje compleet. Workbook-objecten zijn niet thread-safe voor delen, maar niets houdt één onafhankelijke werkmap per workerthread tegen, wat batchconversie netjes parallelliseert. En wanneer uitvoer naar HTTP gaat in plaats van naar schijf, combineren de TStream-opslag-overloads met StreamingWrite zodat een grote respons nooit als tijdelijk bestand materialiseert. Eén operationele voetnoot geldt: het opslaan naar stream schrijft vanaf de huidige positie zonder terug te spoelen, dus zet Position := 0 voordat je de stream aan het responseframework overhandigt. Het artikel over streaming write en batchtaken werkt dat server-side patroon verder uit, en het artikel over database-export laat zien waar deze hendels passen in een dataset-gestuurd rapport
Bewaar ten slotte één worst-case-fixture per rapportfamilie en meet de tijd ervan in CI. Prestatieregressies in documentgeneratie kondigen zichzelf zelden aan. Een stijl toegevoegd binnen een lus of een probe vervangen door een volledige Open verandert functioneel niets, en de nachtelijke batch duurt gewoon veertig minuten langer. Een getimede test op een representatieve fixture van een half miljoen cellen verandert die drift in een rode build in plaats van een operations-incident
Evaluatiebuilds, demoprojecten met een voorbeeld van bulkgeneratie, en de volledige API-referentie zijn beschikbaar op de HotXLS Delphi Component-pagina