HotXLS, den native Excel-komponentbiblioteket for Delphi og C++Builder, pakker ut flere XLSX-regneark samtidig fra én åpen ZIP-pakke. Mekanismen er TZipReadGate, en liten klasse i lxZipArchive.pas som holder pakkestrømmen pluss én kritisk seksjon og eksponerer nøyaktig én metode. Den serialiserer seek-og-read-paret. Alt over det paret kjører samtidig
Problemet som tvang frem dette designet er ett hver Delphi-utvikler som har åpnet en stor arbeidsbok har møtt. En 80 MB xlsx er 80 MB deflatert XML, og regnearkdelene inni den ekspanderer omtrent fem til ti ganger. Hvis den åpne stien din trekker ut hvert regneark til en minnestrøm før den parser det, betaler du for de utpakkede bytene oppå arbeidsboken du bygger, og toppen ankommer før en eneste celle er opprettet. Denne artikkelen handler om samtidigheten på pakkenivå som fjerner det mellomlagringstrinnet. Allokeringstaket som sitter over det er dekket i artikkelen om parallell XLSX-parsing og minnehåndtereren, og API-et for les-én-gang, aldri-materialiser er dekket i gjennomgangen av den strømmende direktelesningen
Hvorfor mellomlagret den gamle åpne stien hvert regneark i RAM
Den opprinnelige parallelle åpningen i HotXLS var en tre-fase-pipeline, og den midterste fasen var den eneste som kjørte på arbeidere. Fase A gikk gjennom arklisten serielt, opprettet hvert regneark, leste dens relasjonsdel, og kopierte hele den utpakkede regneark-XML-en inn i en privat TMemoryStream. Fase B viftet ParseWorksheetXml ut over poolen. Fase C gikk tilbake til arkivet på den kallende tråden for de små satellittdelene: kommentarer, trådede kommentarer, tegninger, diagrammer, tabeller. Den formen ble valgt av en oppgitt grunn. Header-kommentaren i lxParallelParse.pas pleide å si, med rett ut sagte ord, at zip-arkivet og dets inflate-tilstand ikke er trådsikre, og de interne notatene gikk lenger: ikke bry deg med å låse arkivet, fordi når inflate-tilstandsmaskinen først er serialisert per oppføring, gir låsen ingenting. Fase A eksisterte for å holde hver arkivberøring på én tråd. Kostnaden var at en arbeidsbok med åtte travle ark holdt åtte fullstendig utpakkede regneark-XML-buffere i minnet samtidig, og de bufferne er de største flyktige objektene i hele den åpne stien
Kan to tråder pakke ut fra én ZIP-strøm?
Ja, og den gamle vurderingen var feil på en spesifikk, lokaliserbar måte: den slo sammen to forskjellige tilstandsbiter til én setning. Inflate-tilstand er genuint ikke delbar. En zlib z_stream bærer det glidende vinduet, Huffman-tabellene og bitposisjonen for ett komprimert medlem, og to tråder som skyver bytes gjennom samme én produserer søppel. Den underliggende bytekilden er et helt annet spørsmål, og svaret der er at en filstrøm har nøyaktig én bit muterbar delt tilstand verdt å beskytte, dens posisjonspeker
ZIP-containeren gjør skillet lovlig. Hvert medlem i et ZIP-arkiv er komprimert uavhengig: sin egen lokale filheader, sin egen deflate-bitstrøm ved sin egen DataOffset, sin egen CRC32 og størrelser i den sentrale katalogen. Det finnes ingen delt ordbok som spenner over medlemmer slik en solid 7z-blokk har, så oppføring N kan pakkes ut uten å røre oppføring M. Gi hver arbeider sin egen z_stream over sitt eget byteintervall, og det eneste de kolliderer på er seek-en. Den kollisjonen er det TZipReadGate fjerner, og hele klassen er kort nok til å lese i én skjerm
type
TZipReadGate = class
private
FBaseStream: TStream;
FLock: TRTLCriticalSection;
public
constructor Create(ABaseStream: TStream);
destructor Destroy; override;
function ReadAt(AOffset: Int64; var Buffer; Count: Longint): Longint;
end;
function TZipReadGate.ReadAt(AOffset: Int64; var Buffer;
Count: Longint): Longint;
begin
if Count <= 0 then
begin
Result := 0;
Exit;
end;
EnterCriticalSection(FLock);
try
FBaseStream.Position := AOffset;
Result := FBaseStream.Read(Buffer, Count);
finally
LeaveCriticalSection(FLock);
end;
end;
Hva TZipReadGate beskytter, og hva den bevisst ikke gjør
TZipReadGate.ReadAt vokter én udelelig operasjon, å posisjonere den delte strømmen og lese fra den, og ingenting annet. TZipArchive.OpenArchive konstruerer gaten over FInputStream når den sentrale katalogen har parset uten feil, og TZipArchive.Close frigir den. Arkiver åpnet for skriving får aldri en. Hver lesing en arbeider utfører på pakken kanaliseres derfor gjennom én kritisk seksjon holdt for varigheten av én bufret lesing
Alt annet holder seg utenfor låsen fordi det allerede er privat eller allerede uforanderlig. TZipSubStream holder sin egen FPosition, så hver arbeider sporer sitt eget sted i sin egen oppføring. TZLibStream-en som TZipEntry.GetStream bygger over den delstrømmen er per oppføring, opprettet med windowBits på -15 for rå deflate, og aldri delt. Den sentrale katalogen er fullstendig parset før noen arbeider starter, inkludert hver lokal header, så GetEntryByName er et skrivebeskyttet hash-oppslag når samtidighet begynner. Ruteinen selv er tre linjer i TZipSubStream.Read, og den gate-løse grenen er det som holder hver eksisterende enkelttrådet klient på den gamle kodestien
function TZipSubStream.Read(var buffer; Count: longint): longint;
var
rest: Int64;
rc: longint;
begin
rest := FSize - FPosition;
if (Count > rest) then
Count := rest;
if FReadGate <> nil then
rc := FReadGate.ReadAt(FOffset + FPosition, buffer, Count)
else
begin
FBaseStream.Position := FOffset + FPosition;
rc := FBaseStream.Read(buffer, Count);
end;
FPosition := FPosition + rc;
Result := rc;
end;
Hvor mye koster gaten under kontensjon?
Mindre enn frasen «global lås på arkivet» antyder, på grunn av granulariteten TZLibStream tilfeldigvis bruker. Inndatabufferen dens er BufferSize, definert som $4000, så ReadInputBuffer trekker inn 16 KB komprimerte bytes per refyll og gir dem til zng_inflate. Én låseervervelse dekker derfor 16 KB deflate-inndata, som for regneark-XML ekspanderer til noe i størrelsesorden 100 KB markup som arbeideren deretter dekoder og parser uten å holde noe. Låsen holdes for en posisjonert lesing mot operativsystem-cachen; arbeidet den porter er målt i millisekunder
Den ærlige grensen er der det forholdet snur. Oppføringer lagret snarere enn deflaterte leser gjennom gaten én-til-én uten noe inflate-arbeid til å skjule ventetiden, så en pakke full av lagrede medlemmer ville serialisere mye hardere. En kald fil på treg media utvider den kritiske seksjonen, fordi lesingen inni den nå er en reell disktransfer snarere enn et cache-treff. Og forbi en håndfull arbeidere er ikke gaten det du treffer først uansett: regnearkparsing er allokeringstungt, og Delphi-minnehåndtereren serialiserer allokeringer på tvers av tråder godt før lesegaten blir begrensningen. Det er derfor TXLSXWorkbook.ParallelParseThreads som standard bruker et automatisk tak i stedet for én tråd per kjerne
Arbeiderkroppen, og drenerings-løkken det er lett å glemme
Med gaten på plass slettet HotXLS Fase A-mellomlagringen helt. Arbeideren åpner nå sin egen oppføringsstrøm og mater den rett til parseren. To flyktige felt bærer inndataene: FParZip holder arkivet for varigheten av den parallelle fasen, FParSheetPartNames holder delnavnene, og begge tømmes i finally-blokken slik at ingen forlept peker overlever en mislykket åpning. Strømmen som kommer tilbake fra TZipArchive.OpenFile er en TZipVerifiedStream som pakker inn en TZLibStream som pakker inn en TZipSubStream, og å frigi den ytre frigjør kjeden
procedure TXLSXWorkbook.ParseSheetJob(AIndex: Integer);
var
Stream: TStream;
DrainBuffer: array [0..32767] of Byte;
PartName: WideString;
begin
PartName := WideString(FParSheetPartNames[AIndex]);
Stream := FParZip.OpenFile(PartName);
if Stream = nil then
Exit;
try
ParseWorksheetXml(Stream, FSheets.ByPos[AIndex], FParSst,
FParRels[AIndex], FParFontMap, FParFillMap, FParBorderMap,
FParNumFmtMap, FParAlignMap, FParProtMap, FParDateMap);
// Consume any trailing bytes so the ZIP entry size and CRC are verified.
while Stream.Read(DrainBuffer, SizeOf(DrainBuffer)) > 0 do
;
finally
Stream.Free;
end;
end;
Drenerings-løkken er detaljen en direkte port av den gamle koden ville slippe, og å slippe den stille deaktiverer integritetssjekking. TZipVerifiedStream akkumulerer en løpende CRC32 mens bytes passerer gjennom, og kaller VerifyComplete bare når posisjonen når den ukomprimerte størrelsen registrert i den sentrale katalogen; det er der størrelsesavvik- og CRC32-avvik-unntakene kommer fra, pluss en én-byte prober-lesing som fanger en oppføring lengre enn erklært. En XML-leser stopper ved det avsluttende elementet og etterlater vanligvis et linjeskift eller noen få bytes trailing whitespace ulest, så uten dreneringen når posisjonen aldri den erklærte størrelsen og sjekkene utløses aldri. Å lese resten inn i en scratch-buffer koster ingenting og gjenoppretter dem. Da mellomlagringsstrømmene eksisterte, gjorde XlsxCopyStreamAll dette ved en tilfeldighet
Hva som fortsatt kjøres serielt, og flagget som slår alt av
Fase A overlever, minus utvinningen. Den oppretter fortsatt hvert regneark og leser dets relasjoner på den kallende tråden, som er det som lar hvert delt kart være uforanderlig når arbeidere starter. Fase C går fortsatt gjennom arkene serielt etterpå for kommentarer, tegninger, diagrammer og tabeller, og vakten dens endret seg fra en null-sjekk på det gamle mellomlagringsarrayet til zip.Exists mot delnavnet. De delte skrivebeskyttede inndataene arbeiderne rører, den delte strengtabellen og cellXf-kartene, er komplette før Fase B begynner og skrives aldri under den
var
Wb: TXLSXWorkbook;
begin
Wb := TXLSXWorkbook.Create;
try
Wb.ParallelParse := True; // default; False forces one sheet at a time
Wb.ParallelParseThreads := 4; // 0 selects the automatic cap
Wb.Open('quarterly-consolidation.xlsx');
// ... workbook is identical either way ...
finally
Wb.Free;
end;
end;
Å sette ParallelParse til False før Open sender ut samme jobbprosedyre med et trådantall på én, og RunParallelJobs degenererer til en ren løkke på den kallende tråden. Det er verdt å vite av to grunner: det er det ene-linjes svaret hvis en trådingsbekymring noensinne dukker opp i felten, og det betyr at den serielle og den parallelle stien deler én kropp av parsing-kode i stedet for å divergere. Arbeider-unntak fanges, den laveste jobbindeksen vinner, og feilen kastes på nytt på den kallende tråden etter at hver arbeider har sluttet seg til, så et korrupt regneark dukker fortsatt opp som ett unntak på det forventede stedet. Generell tilpasning av den omkringliggende åpne stien er dekket i veiledningen om ytelse for store arbeidsbøker i Delphi
Lesegaten, den parallelle åpne-fasen og den strømmende oppføringstilgangen beskrevet her leveres som del av standard HotXLS Excel-komponent for Delphi og C++Builder, med full kildekode; produktsiden bærer den fullstendige TXLSXWorkbook-referansen inkludert de parallelle åpne-egenskapene