HotXLS, natyvi Excel komponentų biblioteka Delphi ir C++Builder platformoms, vienu metu išplečia kelis XLSX darbalapius iš vieno atverto ZIP paketo. Mechanizmas — TZipReadGate, nedidelė klasė modulyje lxZipArchive.pas, kuri laiko paketo srautą ir vieną kritinę sekciją bei atveria lygiai vieną metodą. Ji suserializuoja pozicionavimo-ir-skaitymo porą. Viskas virš tos poros vyksta lygiagrečiai
Problema, privertusi šį dizainą, — tokia, su kuria susidūrė kiekvienas Delphi kūrėjas, kada nors atvėręs didelę darbaknygę. 80 MB xlsx yra 80 MB suglaudinto XML, o darbalapio dalys jo viduje išsiplečia maždaug penkis–dešimt kartų. Jei jūsų atvėrimo kelias ištraukia kiekvieną darbalapį į atminties srautą prieš jį analizuojant, mokate už išplėstus baitus virš darbaknygės, kurią kuriate, o pikas atvyksta prieš sukuriant nė vieną ląstelę. Šis straipsnis — apie paketo lygio lygiagretumą, pašalinantį šį tarpinį žingsnį. Paskirstytuvo riba virš jo aprašyta straipsnyje apie lygiagrečią XLSX analizę ir atminties valdytoją, o skaityk-vieną-kartą, niekada-nematerializuok API aprašyta straipsnyje apie srautinį tiesioginį skaitytuvą
Kodėl senasis atvėrimo kelias talpino kiekvieną darbalapį RAM atmintyje
Originalus lygiagretus atvėrimas HotXLS buvo trijų fazių vamzdynas, o vidurinė fazė buvo vienintelė, veikusi darbuotojuose. Fazė A nuosekliai ėjo per lapų sąrašą, kūrė kiekvieną darbalapį, skaitė jo ryšio dalį ir kopijavo visą išplėstą darbalapio XML į privatų TMemoryStream. Fazė B paskleisdavo ParseWorksheetXml per baseiną. Fazė C grįždavo į archyvą iškviečiančioje gijoje mažoms palydovinėms dalims: komentarams, gijų komentarams, piešiniams, diagramoms, lentelėms. Ši forma buvo pasirinkta dėl nurodytos priežasties. Antraštės komentaras lxParallelParse.pas anksčiau tiesiogiai sakydavo, kad zip archyvas ir jo išplėtimo būsena nėra saugūs giją, o vidiniai užrašai ėjo dar toliau: neverta rakinti archyvo, nes kai tik išplėtimo būsenos mašina suserializuota kiekvienam įrašui, spyna nieko nenupirkta. Fazė A egzistavo tam, kad kiekvieną archyvo prisilietimą laikytų vienoje gijoje. Kaina buvo ta, kad darbaknygė su aštuoniais užimtais lapais vienu metu laikė aštuonis visiškai išplėstus darbalapio XML buferius atmintyje, ir tie buferiai — didžiausi laikini objektai visame atvėrimo kelyje
Ar dvi gijos gali išplėsti iš vieno ZIP srauto?
Taip, ir senasis sprendimas buvo neteisingas konkrečiu, aptinkamu būdu: jis sulydė du skirtingus būsenos elementus į vieną sakinį. Išplėtimo būsena iš tikrųjų nedalinamasi. zlib z_stream neša slystantį langą, Hafmano lenteles ir bitų poziciją vienam suglaudintam nariui, o dvi gijos, stumiančios baitus per tą pačią, gauna šiukšles. Pagrindinis baitų šaltinis — visai kitas klausimas, ir atsakymas ten toks, kad failo srautas turi lygiai vieną kintamą bendrą būsenos elementą, vertą apsaugoti — jo pozicijos žymeklį
ZIP konteineris atskyrimą padaro teisėtą. Kiekvienas narys ZIP archyve suglaudintas nepriklausomai: savo vietinė failo antraštė, savo deflate bitų srautas savo DataOffset, savo CRC32 ir dydžiai centriniame kataloge. Nėra jokio bendro žodyno, apimančio narius, taip, kaip solidus 7z blokas, todėl įrašas N gali būti išplėstas, neliečiant įrašo M. Kiekvienam darbuotojui duokite savo z_stream per savo baitų intervalą, ir vienintelis dalykas, dėl kurio jie susikerta — pozicionavimas. Ta kolizija — tai, ką pašalina TZipReadGate, ir visa klasė pakankamai trumpa, kad ją būtų galima perskaityti vienoje ekrano dalyje
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;
Ką apsaugo TZipReadGate, ir ko jis sąmoningai neapsaugo
TZipReadGate.ReadAt apsaugo vieną nedalomą operaciją, bendro srauto pozicionavimą ir skaitymą iš jo, ir nieko daugiau. TZipArchive.OpenArchive sukonstruoja vartus virš FInputStream, kai centrinis katalogas sėkmingai išanalizuotas, o TZipArchive.Close juos paleidžia. Archyvai, atverti rašymui, jų niekada negauna. Kiekvienas skaitymas, kurį darbuotojas atlieka pakete, todėl praeina per vieną kritinę sekciją, laikomą vieno buferizuoto skaitymo trukmei
Viskas kita lieka už spynos ribų, nes tai jau privatu arba jau nekintama. TZipSubStream laiko savo pačios FPosition, todėl kiekvienas darbuotojas seka savo vietą savo pačios įraše. TZLibStream, kurį TZipEntry.GetStream sukuria virš to poįrašio srauto, yra vieno įrašo, sukurtas su windowBits lygiu -15 žaliam deflate, ir niekada nesidalinamas. Centrinis katalogas pilnai išanalizuotas prieš pradedant bet kuriam darbuotojui, įskaitant kiekvieną vietinę antraštę, todėl GetEntryByName yra tik-skaityti maišos paieška, kai prasideda lygiagretumas. Pats maršrutizavimas — trys eilutės TZipSubStream.Read, o šaka be vartų kaip tik ir laiko kiekvieną esamą vienos gijos iškviečiantįjį senajame kodo kelyje
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;
Kiek kainuoja vartai esant konkurencijai?
Mažiau, nei rodytų frazė „globali spyna ant archyvo“, dėl granuliuotumo, kurią atsitiktinai naudoja TZLibStream. Jos įvesties buferis — BufferSize, apibrėžtas kaip $4000, todėl ReadInputBuffer ištraukia 16 KB suglaudintų baitų kiekvienam papildymui ir perduoda juos zng_inflate. Vienas spynos gavimas todėl apima 16 KB deflate įvesties, kuri darbalapio XML atveju išsiplečia į kažką maždaug 100 KB žymėjimo, kurį darbuotojas tada dekoduoja ir analizuoja, nieko nelaikydamas. Spyna laikoma pozicionuotam skaitymui prieš operacinės sistemos podėlį; darbas, kurį ji riboja, matuojamas milisekundėmis
Sąžininga riba — ten, kur šis santykis apsiverčia. Įrašai, saugomi be glaudinimo, o ne suglaudinti, skaitomi per vartus vienas su vienu, be jokio išplėtimo darbo, kuris paslėptų delsą, todėl paketas, pilnas saugomų narių, serializuotųsi kur kas sunkiau. Šaltas failas lėtoje laikmenoje išplečia kritinę sekciją, nes skaitymas jos viduje dabar — tikras disko perdavimas, ne podėlio pataikymas. O virš saujelės darbuotojų vartai — ne tai, ką pirmiausia pasieksite: darbalapio analizė gausi paskirstymui, o Delphi atminties valdytojas suserializuoja paskirstymus per gijas gerokai anksčiau, nei skaitymo vartai taps riba. Kaip tik dėl to TXLSXWorkbook.ParallelParseThreads pagal nutylėjimą turi automatinę ribą, o ne po vieną giją kiekvienam branduoliui
Darbuotojo kūnas, ir nusausinimo ciklas, kurį lengva pamiršti
Su vartais vietoje, HotXLS visiškai ištrynė A fazės talpinimą. Darbuotojas dabar atveria savo pačios įrašo srautą ir tiesiogiai jį perduoda analizatoriui. Du laikini laukai neša įvestis: FParZip laiko archyvą lygiagretaus etapo trukmei, FParSheetPartNames laiko dalies pavadinimus, ir abu išvalomi finally bloke, todėl joks pasenęs rodyklė nepergyvena nepavykusio atvėrimo. Srautas, grįžtantis iš TZipArchive.OpenFile, yra TZipVerifiedStream, apgaubiantis TZLibStream, apgaubiantį TZipSubStream, o išorinio paleidimas paleidžia visą grandinę
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;
Nusausinimo ciklas — detalė, kurią tiesioginis senojo kodo perkėlimas nuleistų, o jo nuleidimas tyliai išjungia vientisumo tikrinimą. TZipVerifiedStream kaupia bėgantį CRC32, kai baitai praeina, ir iškviečia VerifyComplete tik tada, kai jos pozicija pasiekia nesuglaudintą dydį, užregistruotą centriniame kataloge; kaip tik iš to atsiranda dydžio nesutapimo ir CRC32 nesutapimo išimtys, plius vieno baito zondavimo skaitymas, gaunantis įrašą, ilgesnį nei deklaruota. XML skaitytuvas sustoja ties uždarančiu elementu ir dažniausiai palieka naują eilutę ar kelis baitus nepaskaitytų tarpo simbolių, todėl be nusausinimo pozicija niekada nepasiekia deklaruoto dydžio, ir patikrinimai niekada nesuveikia. Likučio skaitymas į juodraštinį buferį nieko nekainuoja ir juos atkuria. Kai talpinimo srautai egzistavo, XlsxCopyStreamAll tai darė atsitiktinai
Kas vis dar veikia nuosekliai, ir vėliavėlė, viską išjungianti
Fazė A išgyvena, minus ištraukimą. Ji vis dar kuria kiekvieną darbalapį ir skaito jo ryšius iškviečiančioje gijoje, o tai palieka kiekvieną bendrą žemėlapį nekintamą, kai prasideda darbuotojai. Fazė C vis dar nuosekliai eina per lapus po to komentarams, piešiniams, diagramoms ir lentelėms, o jos apsauga pasikeitė nuo nulinio patikrinimo senajame talpinimo masyve į zip.Exists pagal dalies pavadinimą. Bendros tik-skaityti įvestys, kurias liečia darbuotojai, bendra eilučių lentelė ir cellXf žemėlapiai, yra pilnos prieš prasidedant B fazei ir niekada nerašomos jos metu
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 nustatymas į False prieš Open paskirsto tą pačią darbo procedūrą su vienos gijos skaičiumi, o RunParallelJobs išsigimsta į paprastą ciklą iškviečiančioje gijoje. Tai verta žinoti dėl dviejų priežasčių: tai — vienos eilutės atsakymas, jei giją susijęs rūpestis kada nors iškiltų lauke, ir tai reiškia, kad nuoseklus ir lygiagretus keliai dalijasi vienu analizės kodo kūnu, o ne skiriasi. Darbuotojo išimtys fiksuojamos, mažiausias darbo indeksas laimi, o klaida iš naujo iškeliama iškviečiančioje gijoje po to, kai kiekvienas darbuotojas prisijungia, todėl sugadintas darbalapis vis tiek pasirodo kaip viena išimtis laukiamoje vietoje. Bendras aplinkinio atvėrimo kelio derinimas aprašytas straipsnyje apie didelės darbaknygės našumą Delphi
Skaitymo vartai, lygiagretaus atvėrimo etapas ir srautinis įrašo pasiekiamumas, aprašyti čia, dalyvauja standartiniame HotXLS Excel komponente Delphi ir C++Builder platformoms, su pilnu šaltiniu; produkto puslapyje pateikta pilna TXLSXWorkbook dokumentacija, įskaitant lygiagretaus atvėrimo savybes