Teknisk artikel

Ydeevne for store Excel-projektmapper i Delphi med HotXLS

Når en eksport på 300.000 rækker sprænger sit hukommelsesbudget, får rækketallet som regel skylden. Rækketallet er som regel uskyldigt. De dyre dele af en stor projektmappe er dem, der skabes som en sideeffekt: en stilpulje, der vokser med én post pr. celle, fordi formatering blev tilføjet inde i løkken, ark-XML samlet som én kæmpe streng ved gemmetidspunktet, en million identiske formellegemer gemt ét ad gangen. HotXLS, losLabs native Delphi-bibliotek til XLS- og XLSX-filer, giver dig en specifik håndtag til hver af disse omkostninger. Ingen af dem er slået til som standard, fordi hver især ændrer en afvejning, så at vide, hvilket håndtag der matcher hvilket symptom, er den egentlige ydeevne-færdighed

Hvor en stor projektmappe bruger hukommelse

Der er to adskilte hukommelsesregimer at ræsonnere om. Under generering vokser cellemodellen i hukommelsen med hver celle, du rører: værdier, formater og formler bliver alle til objekter eller puljeposter. Under gemning gengiver standard-XLSX-stien desuden hvert regnearks XML til en wide-streng, før den komprimeres ind i zip-beholderen, så spidsforbruget er modellen plus det største arks serialiserede form. Et job, der overlever byggeløkken og derefter dør inde i SaveAs, rammer det andet regime, ikke det første, og løsningen på det ene gør intet for det andet

To hukommelsesregimer i et Delphi HotXLS storprojektmappejob: den in-memory cellemodel bygget af genereringsloopet, plus det største arks serialiserede XML-streng under en standardgemning, som StreamingWrite fjerner
Byggeløkken og gem-kaldet fejler i to forskellige hukommelsesregimer, så StreamingWrite flader kun gemmetidens spids ud, mens build-vejens hukommelse behøver style-pool- og callback-håndtagene

Filstørrelse følger en beslægtet regel: celler er kun én bidragyder, ved siden af stilarter, delte strenge, formler, billeder og kommentarer. En revisionsgennemgang med ForEachCell og optællinger fra per-ark-samlingerne fortæller dig, hvilken ressource der reelt dominerer en problemfil, før du optimerer den forkerte. Én måle-finesse: Sheet.Cells.Count på XLSX-siden rapporterer antallet af instantierede celler i det sparsomme lager, ikke arealet af det brugte område. Et ark, hvis data optager et rektangel på 1000 gange 50 med halvdelen af cellerne tomme, tæller cirka 25.000, ikke 50.000. Den skelnen betyder noget, når man sammenligner en kundes "kæmpe" fil med sine egne fixtures, fordi det brugte områdes areal og den faktiske cellebefolkning kan afvige med en størrelsesorden i sparsomme finansielle layouts

StreamingWrite retter gemmestien, ikke byggestien

At sætte TXLSXWorkbook.StreamingWrite := True skifter SaveAs til en streaming-serialiser, der skriver ark-XML direkte ind i zip-strømmen og fjerner den per-ark-strengmellemstation. Den er som standard False af hensyn til adfærdskompatibilitet, og at slå den til er en ét-linje-ændring:

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;   // ark-XML strømmer ind i zip-beholderen
  Book.SaveAs('bulk.xlsx');
finally
  Book.Free;
end;

Vær præcis om, hvad dette køber: cellemodellen, løkken bygger, optager præcis lige så meget hukommelse som før. StreamingWrite flader gemmetids-spidsen ud, hvilket er forskellen mellem et batch-job, der fuldføres, og ét, der fejler ved 95%-mærket. Hvis selve byggeløkken opbruger hukommelsen, er de håndtag, du har brug for, de næste to

Stilpuljer: tilføj én gang, genbrug indekset

XLSX-formatering i HotXLS er poolbaseret: Book.Fonts.Add(...), Fills.AddSolid(...) og Borders.Add(...) returnerer et 0-baseret puljeindeks, som celler refererer. At kalde Fonts.Add med identiske parametre inde i en løkke bliver dedupliceret, så det spilder tid frem for plads. Alignments.Add opfører sig anderledes: den returnerer et nyt objekt pr. kald, så per-celle-justeringsoprettelse vokser puljen lineært med rækketallet. Én vane dækker begge tilfælde. Opløs hvert puljeindeks én gang, uden for løkken, og tildel indekser inde i den

HotXLS Delphi style pool-forbrug sammenlignet: et friskt Alignments.Add-objekt oprettet én gang pr. række vokser poolen lineært, mens et hoistet Fonts.Add-indeks opløst én gang over loopet genbruges af hver celle med det nulbaserede indeks forskudt én
Opløs hvert font-, fill-, kant- og justeringsindeks én gang uden for løkken, og tildel derefter det 0-based pool-indeks forskudt med én indeni
// løft puljeopslag ud af den hede løkke
HeaderFont := Book.Fonts.Add('Calibri', 11, True, False);   // 0-baseret puljeindeks
for C := 1 to 24 do
  Sheet.Cells[1, C].FontIndex := HeaderFont + 1;            // celler gemmer 1-baseret; 0 = standard

+ 1'et er ikke en tastefejl, og at glemme det er den klassiske symptomgenererende fejl her: puljerne uddeler 0-baserede indekser, mens celle-side-egenskaberne behandler 0 som "standard", så hvert puljeindeks skal forskydes med én ved tildeling. Får man det galt ved udeladelse, gengives overskrifterne i det stille i projektmappens standardskrift, en defekt ingen bemærker før branding-gennemgangen

Erstat per-celle-Variant-trafik med række-callbacks

Hver Sheet.Cells[R, C].Value := X involverer et celleopslag-eller-opret plus en Variant-tildeling. Ved et par hundrede tusinde celler bliver den per-adgang-overhead målbar i profiler. HotXLS tilbyder bulk-callback-API'er på begge facader (ForEachCell og ForEachRow til læsning, WriteCells og WriteRows til skrivning), der flytter iterationen ind i motoren og giver din kode hele rækker ad gangen:

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 hele skrivningen
    Exit;
  end;
  Values := VarArrayOf([FRows[Row - 1].Account,
                        FRows[Row - 1].PostedOn,
                        FRows[Row - 1].Amount]);
end;

// ét motorkald i stedet for hundredtusindvis af egenskabstræf
Sheet.WriteRows(1, 1, FCount, 3, FillRow);

Callback'ens Skip-flag lader en række stå urørt uden at afbryde, og Cancel afslutter operationen tidligt, hvilket er nyttigt, når kilden er en reader, hvis længde man opdager undervejs. Kombinér WriteRows til opbygningen med StreamingWrite til gemningen, og genereringsstien har intet resterende per-celle-hotspot

Læse-side-håndtag på XLS-facaden

Store gamle .xls-filer har deres eget værktøjssæt. _DisableGraphics := True før Open springer parsing af tegnelaget helt over, hvilket fremskynder indlæsning af projektmapper, der bærer års ophobede figurer og indlejrede billeder. Begrænsningen er hård: tegnelaget er derefter fraværende fra modellen, så det at gemme en sådan projektmappe skriver en fil uden dens tegninger. Reserver dette flag til ren-læsning-analysejob. SetTempDir omdirigerer BIFF-skriverens midlertidige filer, hvilket betyder noget på servere, hvor standard-temp-placeringen har en kvote eller sidder på langsom lagring. UseSharedFormulas grupperer gentagne formellegemer i delte-formel-poster, hvilket krymper filer, hvor en formelkolonne gentager sig ned gennem tres tusind rækker

Læseløkker over XLS-data har en indekseringsfælde, det er værd at flage, fordi den fordobler arbejdet, når den håndteres defensivt, og ødelægger resultater, når den overses: UsedRange rapporterer sine FirstRow-, LastRow-, FirstCol- og LastCol-grænser 0-baseret, mens Cells.Item[Row, Col] er 1-baseret. En scanning, der går gennem det brugte område, skal lægge én til hver koordinat ved celleadgangen, som i Cells.Item[Row + 1, Col + 1], ellers læser den et gitter forskudt diagonalt med én celle, hvilket i det stille dropper den sidste række og kolonne og inkluderer en fantom-første. ForEachCell-callbacken omgår uoverensstemmelsen helt, hvilket er endnu en grund til at foretrække den til hele-ark-scanninger

Sonder filer, før du indlæser dem

Den billigste store-projektmappe-operation er den, du undgår. GetSheetNames på begge facader lister en fils regneark uden at indlæse celledata. XLSX-implementeringen læser kun projektmappe-manifestet inde i zip'en og lader eksplicit projektmappe-instansen stå ufyldt, og XLS-facaden stopper scanningen ved den første understrøms-grænse. Det gør den til det rigtige pre-flight-tjek for "hvilket ark skal dette importjob målrette", og CanReadEncrypted besvarer "er dette en krypteret beholder", før et dødsdømt Open-forsøg

Pre-flight-flow for en ukendt Excel-fil i Delphi med HotXLS: GetSheetNames lister regneark uden at indlæse celldata, en returkode på eller under nul tømmer listen og signalerer fejl, CanReadEncrypted flagger krypterede containere før en dødsdømt Open, og først derefter kører den fulde indlæsning
GetSheetNames og CanReadEncrypted besvarer, hvilket ark der skal rammes, og om containeren er læsbar, før nogen celldata parses
Names := TStringList.Create;
Book := TXLSXWorkbook.Create;
try
  if Book.GetSheetNames('big-unknown.xlsx', Names) <= 0 then
    raise Exception.Create('cannot enumerate sheets');   // fejl rydder listen
  // vælg målarket, og beslut derefter, om et fuldt Open er det værd
finally
  Book.Free;
  Names.Free;
end;

Bemærk return-kode-konventionen: disse sonderende funktioner signalerer fejl med værdier ved eller under nul og tømmer output-listen, så test <= 0 frem for at sammenligne mod én specifik succesværdi

At tilpasse tilgangen til opgaven

For uovervågede pipelines, der genererer mange store filer i rækkefølge, runder to yderligere vaner billedet af. Projektmappeobjekter er ikke trådsikre til deling, men intet forhindrer én uafhængig projektmappe pr. worker-tråd, hvilket parallelliserer batch-konvertering rent. Og når output går til HTTP frem for disk, kombineres TStream-gemme-overloadene med StreamingWrite, så et stort svar aldrig materialiserer sig som en temp-fil. Én driftsmæssig fodnote gælder: stream-gemningen skriver fra den nuværende position uden at spole tilbage, så sæt Position := 0, før du giver strømmen til response-frameworket. Artiklen om streaming-skrivning og batch-job udvikler det server-side-mønster, og artiklen om databaseeksport viser, hvor disse håndtag passer ind i en dataset-drevet rapport

Til sidst, behold én worst-case-fixture pr. rapportfamilie, og tag tid på den i CI. Ydeevneregressioner i dokumentgenerering annoncerer sjældent sig selv. En stil tilføjet inde i en løkke eller en sonde erstattet med et fuldt Open ændrer intet funktionelt, og den natlige batch tager simpelthen fyrre minutter længere. En tidtaget test på en repræsentativ halv-million-celle-fixture forvandler den drift til en rød build i stedet for en driftshændelse

Evalueringsbuilds, demoprojekter med et bulk-genereringseksempel og den fulde API-reference er tilgængelige på siden for HotXLS-Delphi-komponenten