HotXLS, natiivi Excel-komponenttikirjasto Delphille ja C++Builderille, purkaa useita XLSX-työarkkeja samaan aikaan yhdestä avoimesta ZIP-paketista. Mekanismi on TZipReadGate, pieni luokka tiedostossa lxZipArchive.pas, joka pitää pakettivirran plus yhden kriittisen alueen ja paljastaa täsmälleen yhden metodin. Se sarjallistaa haku-ja-luku-parin. Kaikki tuon parin yläpuolella ajetaan rinnakkain
Ongelma, joka pakotti tämän suunnittelun, on sellainen, jonka jokainen suuren työkirjan avannut Delphi-kehittäjä on kohdannut. 80 megatavun xlsx on 80 megatavua deflate-pakattua XML:ää, ja sen sisällä olevat työarkkiosat laajenevat suunnilleen viisi–kymmenkertaisiksi. Jos avauspolkusi erottaa jokaisen työarkin muistivirtaan ennen sen jäsentämistä, maksat puretuista tavuista rakentamasi työkirjan päälle, ja huippu saapuu ennen kuin yhtäkään solua on luotu. Tämä artikkeli käsittelee pakettitason rinnakkaisuutta, joka poistaa tuon välivaiheen. Sen yläpuolella istuva varaajan katto käsitellään artikkelissa rinnakkainen XLSX-jäsennys ja muistinhallinta, ja lue-kerran-älä-koskaan-materialisoi-API käsitellään artikkelissa suoratoistava suora lukija
Miksi vanha avauspolku vaiheisti jokaisen työarkin RAM-muistiin
Alkuperäinen rinnakkainen avaus HotXLS:ssä oli kolmivaiheinen putki, ja keskimmäinen vaihe oli ainoa, joka ajettiin työntekijöillä. Vaihe A käveli arkkilistan sarjallisesti, loi jokaisen työarkin, luki sen suhdeosan ja kopioi koko puretun työarkin XML:n yksityiseen TMemoryStream:iin. Vaihe B levitti ParseWorksheetXml:n poolin yli. Vaihe C palasi arkistoon kutsuvalla säikeellä pienten satelliittiosien vuoksi: kommentit, säikeistetyt kommentit, piirrokset, kaaviot, taulukot. Tuo muoto valittiin ilmoitetusta syystä. Otsikkokommentti tiedostossa lxParallelParse.pas sanoi ennen suoraan, että zip-arkisto ja sen purkutila eivät ole säiekelpoisia, ja sisäiset muistiinpanot menivät pidemmälle: älä vaivaudu lukitsemaan arkistoa, koska kun purkutilakone on kerran sarjallistettu merkinnittäin, lukitus ei osta mitään. Vaihe A oli olemassa pitääkseen jokaisen arkistokosketuksen yhdellä säikeellä. Kustannus oli, että työkirja, jossa oli kahdeksan kiireistä arkkia, piti kahdeksan täysin purettua työarkin XML-puskuria muistissa samanaikaisesti, ja nuo puskurit ovat koko avauspolun suurimmat väliaikaiset objektit
Voiko kaksi säiettä purkaa yhdestä ZIP-virrasta?
Kyllä, ja vanha arvio oli väärä tietyllä, paikannettavalla tavalla: se sulautti kaksi eri tilaosaa yhteen lauseeseen. Purkutila ei todella ole jaettavissa. Zlib-z_stream kantaa liukuvan ikkunan, Huffman-taulukot ja bittiaseman yhdelle pakatulle jäsenelle, ja kaksi säiettä, jotka työntävät tavuja saman läpi, tuottaa roskaa. Taustalla oleva tavulähde on täysin eri kysymys, ja sen vastaus on, että tiedostovirralla on täsmälleen yksi suojaamisen arvoinen muuttuva jaettu tilaosa, sen positiokursori
ZIP-säilö tekee erottelusta laillisen. Jokainen jäsen ZIP-arkistossa pakataan itsenäisesti: oma paikallinen tiedosto-otsikko, oma deflate-bittivirta omassa DataOffset:ssaan, oma CRC32 ja koot keskushakemistossa. Ei ole jäsenten yli ulottuvaa jaettua sanakirjaa samalla tavalla kuin kiinteällä 7z-lohkolla on, joten merkintä N voidaan purkaa koskematta merkintään M. Anna jokaiselle työntekijälle oma z_stream omalle tavualueelleen, ja ainoa, mistä ne törmäävät, on haku. Tuon törmäyksen TZipReadGate poistaa, ja koko luokka on tarpeeksi lyhyt luettavaksi yhdellä ruudulla
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;
Mitä TZipReadGate suojaa ja mitä se tarkoituksella ei suojaa
TZipReadGate.ReadAt vartioi yhtä jakamatonta operaatiota, jaetun virran positioimista ja siitä lukemista, eikä mitään muuta. TZipArchive.OpenArchive rakentaa portin FInputStream:in ympärille heti kun keskushakemisto on jäsennetty onnistuneesti, ja TZipArchive.Close vapauttaa sen. Kirjoittamista varten avatut arkistot eivät koskaan saa sellaista. Jokainen luku, jonka työntekijä suorittaa pakettiin, kulkee siis yhden kriittisen alueen läpi, joka pidetään yhden puskuroidun luvun ajan
Kaikki muu pysyy lukituksen ulkopuolella, koska se on jo yksityistä tai jo muuttumatonta. TZipSubStream pitää oman FPosition:insa, joten jokainen työntekijä seuraa omaa paikkaansa omassa merkinnässään. TZLibStream, jonka TZipEntry.GetStream rakentaa tuon alivirran ympärille, on merkintäkohtainen, luotu windowBits-arvolla -15 raakaa deflatea varten, eikä koskaan jaettu. Keskushakemisto on täysin jäsennetty ennen kuin yksikään työntekijä alkaa, mukaan lukien jokainen paikallinen otsikko, joten GetEntryByName on vain-luku-hakemistohaku siihen mennessä kun rinnakkaisuus alkaa. Reititys itsessään on kolme riviä tiedostossa TZipSubStream.Read, ja portiton haara on se, mikä pitää jokaisen olemassa olevan yksisäikeisen kutsujan vanhalla koodipolulla
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;
Kuinka paljon portti maksaa kilpailussa?
Vähemmän kuin lause "globaali lukitus arkistolla" antaa ymmärtää, koska TZLibStream sattuu käyttämään tiettyä rakeisuutta. Sen syötepuskuri on BufferSize, määritelty $4000:na, joten ReadInputBuffer vetää 16 kilotavua pakattuja tavuja per täyttö ja antaa ne zng_inflate:lle. Yksi lukituksen hankinta kattaa siis 16 kilotavua deflate-syötettä, joka työarkin XML:lle laajenee suuruusluokkaa 100 kilotavuun merkintäkieltä, jonka työntekijä sitten dekoodaa ja jäsentää pitämättä mitään. Lukitus pidetään positioidun luvun ajan käyttöjärjestelmän välimuistia vasten; työ, jota se porttaa, mitataan millisekunneissa
Rehellinen raja on siinä, missä tuo suhde kääntyy. Merkinnät, jotka on tallennettu deflate-pakkauksen sijaan, luetaan portin läpi yksi yhteen ilman purkutyötä, joka piilottaisi viiveen, joten paketti täynnä tallennettuja jäseniä sarjallistuisi paljon kovemmin. Kylmä tiedosto hitaalla medialla leventää kriittisen alueen, koska sen sisällä oleva luku on nyt todellinen levysiirto eikä välimuistiosuma. Ja kourallisen työntekijän jälkeen portti ei ole ensimmäinen asia, johon törmää joka tapauksessa: työarkin jäsennys on varausraskasta, ja Delphi-muistinhallinta sarjallistaa varaukset säikeiden yli hyvin ennen kuin lukuportista tulee rajoite. Siksi TXLSXWorkbook.ParallelParseThreads on oletuksena automaattinen katto yhden säikeen per ydin sijaan
Työntekijän runko ja tyhjennyssilmukka, joka on helppo unohtaa
Portin ollessa paikallaan HotXLS poisti Vaihe A -vaiheistuksen kokonaan. Työntekijä avaa nyt oman merkintävirtansa ja syöttää sen suoraan jäsentäjälle. Kaksi väliaikaista kenttää kantavat syötteet: FParZip pitää arkiston rinnakkaisvaiheen ajan, FParSheetPartNames pitää osanimet, ja molemmat tyhjennetään finally-lohkossa, jotta yksikään vanhentunut osoitin ei selviä epäonnistuneesta avauksesta. Virta, joka tulee takaisin TZipArchive.OpenFile:sta, on TZipVerifiedStream, joka kääriytyy TZLibStream:n ympärille, joka kääriytyy TZipSubStream:n ympärille, ja uloimman vapauttaminen vapauttaa ketjun
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;
Tyhjennyssilmukka on yksityiskohta, jonka vanhan koodin suora siirto pudottaisi, ja sen pudottaminen poistaa eheystarkistuksen hiljaa käytöstä. TZipVerifiedStream kerää juoksevan CRC32:n tavujen kulkiessa läpi ja kutsuu VerifyComplete:a vasta kun sen positio saavuttaa keskushakemistoon tallennetun puretun koon; siitä tulevat kokoristiriitapoikkeus ja CRC32-ristiriitapoikkeus, plus yhden tavun luotausluku, joka nappaa merkinnän, joka on pidempi kuin ilmoitettu. XML-lukija pysähtyy sulkevaan elementtiin ja jättää yleensä rivinvaihdon tai muutaman tavun perässä olevaa tyhjätilaa lukematta, joten ilman tyhjennystä positio ei koskaan saavuta ilmoitettua kokoa eivätkä tarkistukset koskaan laukea. Loppuosan lukeminen luonnospuskuriin ei maksa mitään ja palauttaa ne. Kun vaiheistusvirrat olivat olemassa, XlsxCopyStreamAll teki tätä vahingossa
Mikä ajetaan vielä sarjallisesti, ja lippu, joka kytkee sen kaiken pois
Vaihe A selviää, miinus erottaminen. Se yhä luo jokaisen työarkin ja lukee sen suhteet kutsuvalla säikeellä, mikä on se, mikä jättää jokaisen jaetun kartan muuttumattomaksi heti kun työntekijät alkavat. Vaihe C kävelee yhä arkit sarjallisesti sen jälkeen kommenttien, piirrosten, kaavioiden ja taulukoiden vuoksi, ja sen vartija muuttui vanhan vaiheistustaulukon null-tarkistuksesta zip.Exists:iin osanimeä vasten. Jaetut vain-luku-syötteet, joita työntekijät koskettavat, jaettu merkkijonotaulukko ja cellXf-kartat, ovat valmiit ennen kuin Vaihe B alkaa eikä niihin koskaan kirjoiteta sen aikana
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;
ParallelParse:n asettaminen False:ksi ennen Open:ia lähettää saman työprosessin yhden säikeen määrällä, ja RunParallelJobs rappeutuu tavalliseksi silmukaksi kutsuvalla säikeellä. Tämä kannattaa tietää kahdesta syystä: se on yhden rivin vastaus, jos säikeistysongelma joskus nousee esiin kentällä, ja se tarkoittaa, että sarjallinen ja rinnakkainen polku jakavat yhden jäsennyskoodin rungon sen sijaan, että ne poikkeaisivat toisistaan. Työntekijän poikkeukset kaapataan, alin työindeksi voittaa, ja virhe nostetaan uudelleen kutsuvalla säikeellä sen jälkeen kun jokainen työntekijä liittyy takaisin, joten korruptoitunut työarkki näkyy yhä yhtenä poikkeuksena odotetussa paikassa. Ympäröivän avauspolun yleinen viritys käsitellään artikkelissa suurten työkirjojen suorituskyky Delphissä
Lukuportti, rinnakkainen avausvaihe ja tässä kuvattu suoratoistava merkintäkäyttö toimitetaan osana vakiota HotXLS Excel -komponenttia Delphille ja C++Builderille, täydellä lähdekoodilla; tuotesivu kantaa täyden TXLSXWorkbook-viitteen mukaan lukien rinnakkaisen avauksen ominaisuudet