HotXLS, det opprinnelige Excel-biblioteket for Delphi og C++Builder, tolker (parses) XLSX-regneark på flere tråder gjennom en trefaset innlasting: regneark-XML dekomprimeres serielt, tolkes i parallell, og små deler leses serielt etterpå. Den første utgivelsen av denne funksjonen ga bare 12–25% forbedring, fordi Delphis standard minnebehandler-lås serialiserte arbeidstrådene. Kutt i heap-allokeringer fra omtrent 20 til 9.1 per celle økte den parallelle hastighetsøkningen til ×1.90 på åtte tråder. Denne artikkelen går gjennom målingene, feiltrinnene og de to løsningene som faktisk fungerte
Scenarioet som motiverer alle tre er deprimerende vanlig. En faktureringstjeneste laster inn en mal kunden har utformet i Excel — bedriftens fargetema, miniatyrdiagrammer (sparklines) i en KPI-kolonne, en betinget formateringsregel lagt til av en nyere Excel-versjon — skriver én fakturasum inn i celle B3, og lagrer. Kunden åpner resultatet, og merkevarefargene har gått tilbake til standard Office-blå, miniatyrdiagrammene er borte, og Excel tilbyr å "reparere" filen. Ingenting i koden berørte noen av disse funksjonene. Det gjorde biblioteket, ganske enkelt ved å lagre
Hvordan tolker HotXLS XLSX-regneark i parallell?
HotXLS deler Open inn i tre faser, og bare den midterste kjører på arbeidstråder. Årsaken er ZIP-beholderen: et ZIP-arkiv er én delt inndatastrøm med én inflate-tilstandsmaskin, og den tilstandsmaskinen kan ikke leses av to tråder samtidig. Å pakke den inn i en lås ville være meningsløst, fordi inflate er iboende serielt per oppføring, så en lås ville bare gjenskape seriell kjøring med ekstra overhead. Fase A dekomprimerer derfor hvert regnearks XML til sin egen TMemoryStream mens den fremdeles er enkelttrådet; i vår benchmark-fil tok dette omtrent 4 ms for åtte regnearkdeler, så det er langt fra flaskehalsen. Fase B kjører ParseWorksheetXml for hvert ark på en arbeiderpool, og det er her nesten all innlastingstiden ligger. Fase C går tilbake til ZIP-en serielt for de små delene: kommentarer, tegninger, diagrammer og tabeller
Selve arbeiderpoolen er bevisst enkel. Arbeidere henter jobb-indekser fra en delt teller med InterlockedIncrement, slik at ark av ulik størrelse balanseres naturlig uten noen planlegger. Trådantallet er min(sheet count, CPU cores), det første arbeider-unntaket fanges opp med AcquireExceptionObject og kastes på nytt på hovedtråden etter sammenslåingen (join), og dispatcheren degraderer til en vanlig seriell løkke når det er null eller én jobb. Two properties on TXLSXWorkbook control the feature: ParallelParse gates the pool, and ParallelParseThreads caps the thread count, with 0 meaning automatic. Arbeidsbøker med flere ark er formen som drar nytte av dette, inkludert typen du produserer ved å duplisere et mal-regneark dusinvis av ganger
Hvorfor gjør tilføying av tråder XLSX-tolking tregere i Delphi?
Fordi Delphis standard minnebehandler beskytter sin heap med a global lock, and worksheet parsing is allocation-dense: cells, Variants, and WideStrings by the million. Hver arbeider som berører heapen stiller seg i kø på den låsen, så tråder som ser uavhengige ut i kildekoden kjører nesten én om gangen i praksis. Vår første benchmark gjorde dette smertefullt konkret. På en 8-arks arbeidsbok med 5000 rader ganger 4 kolonner per ark, målt på en i5-11600K (6 kjerner, 12 tråder) under Win64, parallel Open improved by only 12–25% against a plan estimate of at least 40%. Et sveip av trådantall over 2, 3, 4, 6 og 8 tråder ga en flat kurve, og i senere instrumenterte kjøringer var 2-tråderskonfigurasjonen faktisk 26% tregere enn seriell, den klassiske signaturen på to tråder som ping-ponger en omstridt lås
Tre målinger fastslo diagnosen, og hver av dem veltet den forrige intuisjonen. For det første åpnet en bitteliten fil (8 ark med 1 rad) på 1.2 ms, noe som beviser at tolking i hovedsak er 100% av Open og det ikke var noen skjult fast kostnad å klandre. For det andre viste en mikrobenchmark av ren allokeringsstøy at Delphis minnebehandler skalerte bakover: det samme totale volumet av 2 millioner objekt- og AnsiString-allokeringer kjørte 60% tregere på 8 tråder enn på én, mens den samme støyen mot WideString-heapen, som er COM BSTR-allokatoren snarere enn Delphi MM, skalerte til ×3.7. That HotXLS uses WideString throughout turned out to be an accident of history working in our favor. For det tredje viste GetProcessTimes at under en parallell Open var CPU-tiden omtrent lik sanntid: åtte nominelle tråder brukte omtrent 1.3 tråders verdi av CPU. Arbeiderne spant ikke; de sov i minnebehandlerens konfliktsone, blokkert i stedet for opptatt
Den praktiske lærdommen gjelder også utenfor regneark. Hvis en Delphi-arbeidsmengde allokerer mye, raising the thread count does nothing until the allocation rate drops, and it can easily make things worse. Før denne løsningen fortalte vi brukere som justerte ParallelParseThreads den ærlige sannheten: på allokeringsbegrensede filer ga flere tråder nesten ingenting
Hvor kommer 20 heap-allokeringer per celle fra?
En telle-wrapper installert med SetMemoryManager besvarte det spørsmålet presist: omtrent 20 Delphi-MM-allokeringer per celle, med 2.87 millioner av dem på 32 byte eller under. Synderen var ikke celleobjektene i det hele tatt. TXMLScaner.GetTokenValue materialiserte en ny AnsiString ved hvert kall, og den kalles omtrent 15–20 ganger per celle: én gang hver for elementnavn, attributtnavn, attributtverdier og tekstinnhold. På toppen av det, the RTL's UTF8ToWideString route manufactured a temporary UnicodeString intermediate for every conversion. Celleobjekter sto for bare 160 tusen allokeringer, omtrent 8% av totalen, noe som tok livet av vår opprinnelige plan på stedet: vi hadde planlagt å bygge en celleobjekt-pool, og tallene sa at det aldri ville betale seg
Løsningen: token-internering og en UTF-8-dekoder uten mellomtrinn
To målrettede endringer i XML-leseren fjernet mer enn halvparten av allokeringene per celle uten å berøre parserens struktur. Den første er internering av elementnavn. Regneark-XML gjentar et lite ordforråd i det uendelige: row, c, v, r, t, s og en håndfull attributtnavn. InternTokenName beholder en 64-spors hurtigbuffer (cache) med tidligere sette navn, og sammenligner skannerens byggebuffer med en bufret oppføring med TokenEqualsAnsi, a direct byte comparison that allocates nothing. Ved treff returnerer den den bufrede AnsiString, og her er typevalget viktig: AnsiString er referansetelt, så å returnere en bufret instans koster én refcount-økning og null heaptrafikk. WideString has no reference count, and every assignment goes through SysAllocString, so interning WideStrings would save nothing. Internering er bare verdt å gjøre på den referansetelte strengtypen
Den andre endringen retter seg mot celletekst. Den gamle banen bygde et AnsiString-token, ga det til UTF8ToWideString, som bygde en UnicodeString-mellomting, som til slutt ble konvertert til WideString cellen lagrer: to Delphi-MM-allokeringer per tekst-token før den virkelige. Erstatningen, XmlUtf8ToWide(TokenPtr, TokenLen), is a two-pass pure Pascal UTF-8 decoder that reads straight from the scan buffer: pass one measures the UTF-16 length, pass two decodes into a WideString allocated once. Net cost per text token: one COM allocation, zero Delphi-MM allocations. En semantisk merknad for de forsiktige: ved feilformede UTF-8-sekvenser slipper den nye dekoderen byte igjennom i stedet for å erstatte med erstatningstegn slik RTL gjør, noe som bare påvirker hvordan korrupte filer degraderes; på gyldige inndata er utdataene byte-identiske. XML-tegnenheter når aldri dekoderen, fordi skanneren allerede har løst dem opp til UTF-8 i tokenbufferen
Hva ga det, og hvor parallelltolking fremdeles ikke vil hjelpe
De to løsningene kuttet allokeringer per celle fra omtrent 20 til 9.1, og de parallelle tallene beveget seg slik teorien sa de burde. På den samme 8-arks, 5000-raders benchmarken og den samme 6C12T-maskinen, den parallelle 8-trådersforbedringen gikk fra 14% til 47.4%, a ×1.90 speedup over serial. 2-tråderstilfellet svingte fra 26% tregere til 23.6% raskere, og målt CPU-utnyttelse økte fra ×1.0 til ×2.2. Den serielle banen ble omtrent 3% raskere som en bonus, siden færre allokeringer hjelper en enkelt tråd også. The remaining ~9 allocations per cell are roughly half cell objects and half amortized container growth; vi målte dem, anså avkastningen som avtagende og stoppet, med MM-wrapperen klar til å sample på nytt etter kallsted dersom en fremtidig arbeidsmengde rettferdiggjør en ny runde
Grensene er verdt å nevne like tydelig som gevinstene. HotXLS parallelliserer på regneark-granularitet, så en arbeidsbok som er ett gigantisk ark tolkes på én tråd uansett hva ParallelParseThreads sier; for den formen er den strømmende direkte-leseren det beste verktøyet, siden den unngår å materialisere arbeidsboken i det hele tatt. Files whose time goes into Phase C parts, drawings and charts and comments, see less benefit because that phase stays serial by design. Små filer er ikke verdt å tråde i det hele tatt, og det er grunnen til at dispatcheren i det stille kjører serielt for ubetydelige jobb-antall. And the memory manager ceiling has not vanished, only receded: at 9.1 allocations per cell the global lock still taxes the workers, which is why eight threads yield ×1.90 rather than ×4. For det bredere verktøysettet for å kutte innlastings- og lagringstider, inkludert stiler, pooler og bulk-rad-tilbakeslag (callbacks), se vår guide til ytelse for store arbeidsbøker i Delphi
Parallell XLSX-tolking, egenskapene ParallelParse og ParallelParseThreads, og den allokerings-gjerrige XML-leseren beskrevet her leveres som standarddeler av HotXLS Delphi Excel Component, som leser og skriver XLS, XLSX og ODS opprinnelig fra Delphi og C++Builder uten at Excel-automatisering er involvert