HotXLS, izvorna knjižnica komponent Excel za Delphi in C++Builder, hkrati dekompresira več delovnih listov XLSX iz enega odprtega paketa ZIP. Mehanizem je TZipReadGate, majhen razred v lxZipArchive.pas, ki hrani tok paketa plus en kritični odsek in izpostavi natanko eno metodo. Serializira par iskanje-branje. Vse nad tem parom teče sočasno
Problem, ki je vsilil to zasnovo, je nekaj, s čimer se je srečal vsak Delphi razvijalec, ki je odprl velik delovni zvezek. 80 MB xlsx je 80 MB deflated XML, deli delovnih listov znotraj njega pa se razširijo približno pet do desetkrat. Če vaša pot odpiranja izvleče vsak delovni list v pomnilniški tok, preden ga razčleni, plačate za napihnjene bajte poleg delovnega zvezka, ki ga gradite, vrh pa pride, preden je bila ustvarjena ena sama celica. Ta članek se osredotoča na sočasnost na ravni paketa, ki to stopnjo postavljanja odpravi. Zgornjo mejo alokatorja, ki sedi nad njo, pokriva članek o vzporednem razčlenjevanju XLSX in upravitelju pomnilnika, API branja enkrat, brez materializacije, pa pokriva vodič po pretakajočem neposrednem bralniku
Zakaj je stara pot odpiranja postavljala vsak delovni list v RAM
Prvotno vzporedno odpiranje v HotXLS je bil tridelen cevovod, srednja faza pa je bila edina, ki je tekla na delavcih. Faza A je serijsko sprehodila seznam listov, ustvarila vsak delovni list, prebrala njegov del odnosov in kopirala celoten napihnjen XML delovnega lista v zaseben TMemoryStream. Faza B je razpošiljala ParseWorksheetXml čez bazen. Faza C se je vrnila v arhiv na klicni niti za majhne satelitske dele: komentarje, verižene komentarje, risbe, grafikone, tabele. Ta oblika je bila izbrana iz navedenega razloga. Glavni komentar na lxParallelParse.pas je nekoč povedal, z več besedami, da arhiv zip in njegovo stanje dekompresije nista varna za niti, notranje opombe pa so šle dlje: ne trudite se zaklepati arhiva, ker ko je stanje dekompresije serializirano na vnos, ključavnica ne prinese ničesar. Faza A je obstajala, da bi vsak dotik arhiva ostal na eni niti. Cena je bila, da je delovni zvezek z osmimi zaposlenimi listi hkrati hranil osem popolnoma napihnjenih medpomnilnikov XML delovnih listov v pomnilniku, ti medpomnilniki pa so največji prehodni objekti v celotni poti odpiranja
Ali lahko dve niti dekompresirata iz enega toka ZIP?
Da, stara presoja pa je bila napačna na specifičen, izsledljiv način: zložila je dva različna dela stanja v en stavek. Stanje dekompresije resnično ni deljivo. Zlib z_stream nosi drseče okno, Huffmanove tabele in bitni položaj za enega stisnjenega člana, dve niti, ki potiskata bajte skozi isto stvar, pa proizvedeta smeti. Osnovni vir bajtov je povsem drugo vprašanje, odgovor tam pa je, da ima tok datoteke natanko en kos spremenljivega deljenega stanja, vreden zaščite, njegov kazalec položaja
Vsebnik ZIP naredi delitev zakonito. Vsak član v arhivu ZIP je stisnjen neodvisno: svoja lastna lokalna glava datoteke, svoj lasten bitni tok deflate na svojem lastnem DataOffset, svoj lasten CRC32 in velikosti v osrednjem imeniku. Ni deljenega slovarja, ki bi se razprostiral čez člane, kot ima to trden blok 7z, zato je mogoče vnos N dekompresirati, ne da bi se dotaknili vnosa M. Dajte vsakemu delavcu svoj lasten z_stream nad svojim lastnim razponom bajtov, edino, za kar se sprejo, pa je iskanje. To trčenje odstrani TZipReadGate, celoten razred pa je dovolj kratek za branje na enem zaslonu
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;
Kaj TZipReadGate ščiti in česa namerno ne
TZipReadGate.ReadAt varuje eno nedeljivo operacijo, postavitev deljenega toka na položaj in branje iz njega, nič drugega. TZipArchive.OpenArchive zgradi vrata nad FInputStream, ko se osrednji imenik uspešno razčleni, TZipArchive.Close pa jih sprosti. Arhivi, odprti za pisanje, jih nikoli ne dobijo. Vsako branje, ki ga delavec izvede na paketu, se zato izlije skozi eno samo kritično sekcijo, zadržano za trajanje enega medpomnjenega branja
Vse ostalo ostaja zunaj ključavnice, ker je že zasebno ali že nespremenljivo. TZipSubStream hrani svoj lasten FPosition, tako da vsak delavec sledi svojemu mestu v svojem lastnem vnosu. TZLibStream, ki ga TZipEntry.GetStream zgradi nad tem podtokom, je na vnos, ustvarjen z windowBits -15 za surov deflate, in se nikoli ne deli. Osrednji imenik je v celoti razčlenjen, preden začne kateri koli delavec, vključno z vsako lokalno glavo, tako da je GetEntryByName do začetka sočasnosti le za branje razpršeno iskanje. Samo usmerjanje so tri vrstice v TZipSubStream.Read, veja brez vrat pa je tisto, kar drži vsak obstoječi enoniten klicatelj na stari poti kode
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;
Koliko stanejo vrata pri sporu?
Manj, kot nakazuje fraza "globalna ključavnica na arhivu", zaradi granularnosti, ki jo TZLibStream naključno uporablja. Njegov vhodni medpomnilnik je BufferSize, opredeljen kot $4000, tako da ReadInputBuffer na osvežitev povleče 16 KB stisnjenih bajtov in jih preda zng_inflate. Ena pridobitev ključavnice tako pokrije 16 KB vhoda deflate, kar se za XML delovnega lista razširi v nekaj reda 100 KB oznak, ki jih delavec nato dekodira in razčleni, ne da bi kar koli držal. Ključavnica je zadržana za pozicionirano branje proti predpomnilniku operacijskega sistema; delo, ki ga varuje, se meri v milisekundah
Iskrena meja je tam, kjer se to razmerje obrne. Vnosi, shranjeni namesto stisnjeni, berejo skozi vrata ena proti ena brez dela dekompresije, ki bi skril zakasnitev, tako da bi se paket, poln shranjenih članov, veliko trše serializiral. Hladna datoteka na počasnem mediju razširi kritično sekcijo, ker je branje znotraj nje zdaj pravi prenos diska, ne zadetek predpomnilnika. In mimo peščice delavcev vrata sploh niso tisto, kar zadenete najprej: razčlenjevanje delovnega lista je intenzivno z alokacijami, upravitelj pomnilnika Delphi pa serializira alokacije čez niti dolgo preden bralna vrata postanejo omejitev. Zato je TXLSXWorkbook.ParallelParseThreads privzeto samodejna zgornja meja namesto ene niti na jedro
Telo delavca in zanka izpraznitve, ki jo je lahko pozabiti
Z vrati na mestu je HotXLS povsem izbrisal postavljanje faze A. Delavec zdaj odpre svoj lasten tok vnosa in ga poda naravnost razčlenjevalniku. Dve prehodni polji nosita vhode: FParZip hrani arhiv za trajanje vzporedne faze, FParSheetPartNames hrani imena delov, oba pa se počistita v bloku finally, tako da noben zastarel kazalec ne preživi neuspelega odpiranja. Tok, ki se vrne iz TZipArchive.OpenFile, je TZipVerifiedStream, ki ovija TZLibStream, ki ovija TZipSubStream, sprostitev zunanjega pa sprosti verigo
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;
Zanka izpraznitve je podrobnost, ki bi jo neposreden prenos stare kode izpustil, izpustitev pa tiho onemogoči preverjanje celovitosti. TZipVerifiedStream nabira tekoč CRC32, medtem ko bajti tečejo skozi, in pokliče VerifyComplete le, ko njegov položaj doseže nestisnjeno velikost, zabeleženo v osrednjem imeniku; od tam prihajata izjemi za neujemanje velikosti in neujemanje CRC32, plus branje ene sonde po enem bajtu, ki ujame vnos, daljši od izjavljenega. Bralnik XML se ustavi pri zaključnem elementu in običajno pusti neprebran nov znak vrstice ali nekaj bajtov sledečega presledka, tako da brez izpraznitve položaj nikoli ne doseže izjavljene velikosti in se preverjanja nikoli ne sprožijo. Branje preostanka v pomožen medpomnilnik ne stane ničesar in jih obnovi. Ko so obstajali postavitveni tokovi, je to po naključju počel XlsxCopyStreamAll
Kaj še vedno teče serijsko, in zastavica, ki vse to izklopi
Faza A preživi, minus ekstrakcija. Še vedno ustvari vsak delovni list in prebere njegove odnose na klicni niti, kar je tisto, kar pusti vsako deljeno preslikavo nespremenljivo, ko delavci začnejo. Faza C še vedno serijsko sprehodi liste kasneje za komentarje, risbe, grafikone in tabele, njena varovalka pa se je spremenila iz ničelnega preverjanja na starem postavitvenem polju v zip.Exists proti imenu dela. Deljeni vhodi le za branje, ki se jih delavci dotikajo, deljena tabela nizov in preslikave cellXf, so dokončani, preden se faza B začne, in se med njo nikoli ne pišejo
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;
Nastavitev ParallelParse na False pred Open razpošlje isti postopek opravila s številom niti ena, RunParallelJobs pa se degenerira v navadno zanko na klicni niti. To je vredno vedeti iz dveh razlogov: je enovrstičen odgovor, če se v praksi kdaj pojavi skrb glede niti, in pomeni, da si serijska in vzporedna pot delita eno samo telo kode razčlenjevanja namesto da bi se razšli. Izjeme delavcev so zajete, zmaga najnižji indeks opravila, napaka pa se ponovno sproži na klicni niti, potem ko se vsak delavec pridruži, tako da se pokvarjen delovni list še vedno pokaže kot ena izjema na pričakovanem mestu. Splošno nastavljanje okoliške poti odpiranja pokriva vodič po zmogljivosti velikih delovnih zvezkov v Delphiju
Bralna vrata, faza vzporednega odpiranja in dostop do pretakajočih vnosov, opisani tukaj, so del standardne komponente HotXLS Excel za Delphi in C++Builder, s celotnim izvorno kodo; stran izdelka nosi celoten referenčni pregled TXLSXWorkbook, vključno z lastnostmi vzporednega odpiranja