Teknisk artikkel

Ytelse for store Excel-arbeidsbøker i Delphi med HotXLS

Når en eksport på 300 000 rader sprenger minnebudsjettet sitt, får radantallet vanligvis skylden. Radantallet er som regel uskyldig. De kostbare delene av en stor arbeidsbok er de som skapes som en bivirkning: et stilbasseng som vokser med én oppføring per celle fordi formatering ble lagt til inne i løkken, regneark-XML satt sammen som én kjempestreng ved lagring, en million identiske formelkropper lagret én etter én. HotXLS, losLabs native Delphi-bibliotek for XLS- og XLSX-filer, gir deg en spesifikk spak for hver av disse kostnadene. Ingen av dem er aktivert som standard, fordi hver endrer en avveining, så å vite hvilken spak som matcher hvilket symptom, er den egentlige ytelsesferdigheten

Hvor en stor arbeidsbok bruker minne

Det finnes to atskilte minneregimer å resonnere om. Under generering vokser cellemodellen i minnet med hver celle du berører: verdier, formater og formler blir alle objekter eller bassengoppføringer. Under lagring gjengir XLSX-standardstien i tillegg hvert arks XML til en bred streng før den komprimerer den inn i zip-beholderen, så toppforbruket er modellen pluss det største arkets serialiserte form. En jobb som overlever byggeløkken og deretter dør inne i SaveAs, treffer det andre regimet, ikke det første, og løsningen for det ene gjør ingenting for det andre

To minneregimer i en Delphi HotXLS stor arbeidsbokjobb: cellemodellen i minnet bygget av genereringssløyfen, pluss det største arket serialiserte XML-streng under en standard lagring, som StreamingWrite fjerner
Byggeløkken og lagringskallet feiler i to ulike minneregimer, så StreamingWrite flater bare ut lagringstidstopp mens byggeveiminne trenger stilbasseng- og callback-hendlene

Filstørrelse følger en beslektet regel: celler er bare én bidragsyter, ved siden av stiler, delte strenger, formler, bilder og kommentarer. En revisjonsrunde med ForEachCell og tellingene til samlingene per ark forteller deg hvilken ressurs som faktisk dominerer en problemfil før du optimaliserer feil en. Én måledetalj: Sheet.Cells.Count på XLSX-siden rapporterer antallet instansierte celler i det glisne lageret, ikke arealet til det brukte området. Et ark hvis data opptar et rektangel på 1000 ganger 50 med halvparten av cellene tomme, teller omtrent 25 000, ikke 50 000. Det skillet betyr noe når du sammenligner en kundes «enorme» fil mot fixturene dine, fordi arealet til det brukte området og den faktiske cellepopulasjonen kan avvike med en størrelsesorden i glisne finansielle oppsett

StreamingWrite fikser lagringsstien, ikke byggestien

Å sette TXLSXWorkbook.StreamingWrite := True bytter SaveAs til en strømmende serialisator som skriver arkets XML direkte inn i zip-strømmen, og eliminerer strengmellomleddet per ark. Den er False som standard for atferdskompatibilitet, og å skru den på er en énlinjes endring:

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

Vær presis om hva dette kjøper deg: cellemodellen bygget av løkken opptar nøyaktig like mye minne som før. StreamingWrite flater ut toppen ved lagringstidspunktet, som er forskjellen mellom en batchjobb som fullføres, og én som feiler ved 95 %-merket. Hvis selve byggeløkken tømmer minnet, er spakene du trenger, de neste to

Stilbassenger: legg til én gang, gjenbruk indeksen

XLSX-formatering i HotXLS er bassengbasert: Book.Fonts.Add(...), Fills.AddSolid(...) og Borders.Add(...) returnerer en 0-basert bassengindeks som celler refererer til. Å kalle Fonts.Add med identiske parametere inne i en løkke dedupliseres, så det sløser tid snarere enn plass. Alignments.Add oppfører seg annerledes: den returnerer et ferskt objekt per kall, så per-celle-justeringsoppretting vokser bassenget lineært med radantallet. Én vane dekker begge tilfellene. Løs opp hver bassengindeks én gang, utenfor løkken, og tildel indekser inne i den

HotXLS Delphi stil-pool-bruk sammenlignet: et ferskt Alignments.Add-objekt opprettet én gang per rad vokser poolen lineært, mens en heist Fonts.Add-indeks løst én gang over sløyfen gjenbrukes av hver celle med den nullbaserte indeksen forskjøvet med én
Løs hver font-, fyll-, kant- og justeringsindeks én gang utenfor løkken, og tildel deretter den 0-baserte bassengindeksen forskjøvet med én inni den
// løft bassengoppslag ut av den varme løkken
HeaderFont := Book.Fonts.Add('Calibri', 11, True, False);   // 0-basert bassengindeks
for C := 1 to 24 do
  Sheet.Cells[1, C].FontIndex := HeaderFont + 1;            // celler lagrer 1-basert; 0 = standard

+ 1 er ikke en skrivefeil, og å glemme den er den klassiske symptomgenererende feilen her: bassengene deler ut 0-baserte indekser, mens celleegenskapene behandler 0 som «standard», så hver bassengindeks må forskyves med én ved tildeling. Får du det feil ved utelatelse, gjengis overskriftene dine stille i arbeidsbokens standardfont, en defekt ingen legger merke til før merkevaregjennomgangen

Erstatt per-celle Variant-trafikk med radcallback-er

Hver Sheet.Cells[R, C].Value := X involverer et celleoppslag-eller-opprett pluss en Variant-tildeling. Ved noen hundre tusen celler blir den per-tilgang-overheaden målbar i profiler. HotXLS tilbyr bulk-callback-API-er på begge fasadene (ForEachCell og ForEachRow for lesing, WriteCells og WriteRows for skriving) som flytter iterasjonen inn i motoren og gir koden din hele rader om 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;     // stopp hele skrivingen
    Exit;
  end;
  Values := VarArrayOf([FRows[Row - 1].Account,
                        FRows[Row - 1].PostedOn,
                        FRows[Row - 1].Amount]);
end;

// ett motorkall i stedet for hundretusenvis av egenskapstreff
Sheet.WriteRows(1, 1, FCount, 3, FillRow);

Callback-ens Skip-flagg lar en rad stå urørt uten å avbryte, og Cancel avslutter operasjonen tidlig, noe som er nyttig når kilden er en leser hvis lengde du oppdager underveis. Kombiner WriteRows for byggingen med StreamingWrite for lagringen, og genereringsstien har ikke noe gjenværende per-celle-hotspot

Lesesidespaker på XLS-fasaden

Store, gamle .xls-filer har sitt eget verktøysett. _DisableGraphics := True før Open hopper helt over parsing av tegnelaget, noe som fremskynder lasting av arbeidsbøker som bærer år med akkumulerte former og innebygde bilder. Begrensningen er hard: tegnelaget er da fraværende fra modellen, så å lagre en slik arbeidsbok skriver en fil uten tegningene sine. Reserver dette flagget for skrivebeskyttede analysejobber. SetTempDir omdirigerer BIFF-skriverens midlertidige filer, noe som betyr noe på servere der standard midlertidig plassering har en kvote eller ligger på treg lagring. UseSharedFormulas grupperer gjentatte formelkropper inn i delte formel-poster, og krymper filer der en formelkolonne gjentar seg nedover seksti tusen rader

Leseløkker over XLS-data har en indekseringsfelle verdt å flagge fordi den dobler arbeidet når den håndteres defensivt og ødelegger resultater når den overses: UsedRange rapporterer grensene sine FirstRow, LastRow, FirstCol og LastCol 0-basert, mens Cells.Item[Row, Col] er 1-basert. Et søk som går gjennom det brukte området, må legge til én til hver koordinat ved celletilgangen, som i Cells.Item[Row + 1, Col + 1], ellers leser det et rutenett forskjøvet diagonalt med én celle, som stille dropper den siste raden og kolonnen og inkluderer en fantom første. ForEachCell-callback-en unngår misforholdet helt, noe som er enda en grunn til å foretrekke den for hele-ark-søk

Sond filer før du laster dem

Den billigste store-arbeidsbok-operasjonen er den du unngår. GetSheetNames på begge fasadene lister opp en fils regneark uten å laste celledata. XLSX-implementasjonen leser bare arbeidsbokmanifestet inne i zip-en og lar eksplisitt arbeidsbokinstansen stå ufylt, og XLS-fasaden slutter å skanne ved den første understrømgrensen. Det gjør den til riktig forhåndssjekk for «hvilket ark denne importjobben bør sikte mot», og CanReadEncrypted svarer på «er dette en kryptert beholder» før et dømt Open-forsøk

Forhåndssjekksflyt for en ukjent Excel-fil i Delphi med HotXLS: GetSheetNames lister regneark uten å laste celldata, en returkode på eller under null tømmer listen og signaliserer feil, CanReadEncrypted flagger krypterte containere før en dødsdømt Open, og først da kjører full innlasting
GetSheetNames og CanReadEncrypted svarer hvilket ark som skal siktes og om beholderen er lesbar før noen celldata parses
Names := TStringList.Create;
Book := TXLSXWorkbook.Create;
try
  if Book.GetSheetNames('big-unknown.xlsx', Names) <= 0 then
    raise Exception.Create('cannot enumerate sheets');   // feil tømmer listen
  // velg målarket, avgjør deretter om et fullt Open er verdt det
finally
  Book.Free;
  Names.Free;
end;

Legg merke til returkode-konvensjonen: disse sonderende funksjonene signaliserer feil med verdier ved eller under null og tømmer utdatalisten, så test <= 0 i stedet for å sammenligne mot én spesifikk suksessverdi

Å tilpasse tilnærmingen til jobben

For pipelines uten tilsyn som genererer mange store filer i sekvens, runder to vaner til av bildet. Arbeidsbokobjekter er ikke trådsikre for deling, men ingenting hindrer én uavhengig arbeidsbok per arbeidertråd, noe som parallelliserer batchkonvertering rent. Og når utdata går til HTTP i stedet for disk, kombineres TStream-lagringsoverbelastningene med StreamingWrite, slik at et stort svar aldri materialiserer seg som en midlertidig fil. Én driftsmessig fotnote gjelder: strømlagringen skriver fra gjeldende posisjon uten å spole tilbake, så sett Position := 0 før du gir strømmen til responsrammeverket. Artikkelen om strømmeskriving og batchjobber utvikler det server-sidemønsteret, og artikkelen om databaseeksport viser hvor disse spakene passer inn i en datasettdrevet rapport

Til slutt, behold én verste-tilfelle-fixture per rapportfamilie og ta tiden på den i CI. Ytelsesregresjoner i dokumentgenerering annonserer sjelden seg selv. En stil lagt til inne i en løkke eller en sonde erstattet med et fullt Open, endrer ingenting funksjonelt, og nattbatchen tar rett og slett førti minutter lenger. En tidtatt test på en representativ halv-million-celle-fixture gjør den avdriften om til en rød build i stedet for en driftshendelse

Evalueringsbygg, demoprosjekter med et bulk-genereringseksempel, og den fullstendige API-referansen er tilgjengelig på HotXLS Delphi Component-siden