HotXLS, natívna Excel knižnica pre Delphi a C++Builder, analyzuje hárky XLSX na viacerých vláknach prostredníctvom trojfázového načítania: XML hárka sa dekomprimuje sériovo, analyzuje sa paralelne a malé časti sa potom čítajú sériovo. Prvé vydanie tejto funkcie prinieslo zrýchlenie iba o 12 – 25 %, pretože predvolený zámok správcu pamäte v Delphi serializoval pracovné vlákna. Zníženie alokácií haldy z približne 20 na 9,1 na bunku zvýšilo paralelné zrýchlenie na 1,90-násobok na ôsmich vláknach. Tento článok prechádza meraniami, nesprávnymi krokmi a dvoma opravami, ktoré reálne fungovali
Ako HotXLS analyzuje hárky XLSX paralelne?
HotXLS rozdeľuje Open do troch fáz a iba tá stredná beží na pracovných vláknach. Dôvodom je kontajner ZIP: archív ZIP je jedan zdieľaný vstupný tok s jedným stavovým strojom na dekompresiu (inflate), a tento stavový stroj nemôžu čítať dve vlákna naraz. Obaliť ho zámkom by bolo zbytočné, pretože inflate je zo svojej podstaty sériový na každú položku, takže zámok by len reprodukoval sériové vykonávanie s dodatočnou réžiou. Fáza A preto dekomprimuje XML každého hárka do vlastného TMemoryStream, kým je proces stále jednovláknový; v našom testovacom súbore to trvalo približne 4 ms pre osem častí hárkov, čo zďaleka nie je úzkym hrdlom. Fáza B spúšťa ParseWorksheetXml pre každý hárok na fonde pracovných vlákien (worker pool), kde sa odohráva takmer celý čas načítania. Fáza C sa vracia k ZIP súboru sériovo pre malé časti: komentáre, kresby, grafy a tabuľky
Samotný fond pracovných vlákien je zámerne jednoduchý. Vlákna získavajú indexy úloh zo zdieľaného počítadla pomocou InterlockedIncrement, so hárky rôznej veľkosti sa prirodzene vyvažujú bez potreby plánovača. Počet vlákien je určený ako min(počet hárkov, jadrá CPU), prvá výnimka pracovného vlákna je zachytená pomocou AcquireExceptionObject a znova vyvolaná v hlavnom vlákne po dokončení a dispečer prejde na jednoduchú sériovú slučku, keď ide o nula alebo jednu úlohu. Dve vlastnosti na TXLSXWorkbook riadia túto funkciu: ParallelParse spúšťa fond a ParallelParseThreads obmedzuje počet vlákien, pričom 0 znamená automatické nastavenie. Profitujú najmä zošity s viacerými hárkami, vrátane tých, ktoré vytvoríte duplikovaním šablóny hárka desiatkykrát
var
Book: TXLSXWorkbook;
begin
Book := TXLSXWorkbook.Create;
try
Book.ParallelParse := True; // enable the parallel worker pool
Book.ParallelParseThreads := 0; // 0 = auto: min(sheets, CPU cores)
if Book.Open('quarterly-ledger.xlsx') <= 0 then
raise Exception.Create('open failed');
// ... read cells as usual; the workbook is fully materialized ...
finally
Book.Free;
end;
end;
Prečo pridanie vlákien spomaľuje analýzu XLSX v Delphi?
Pretože predvolený správca pamäte v Delphi chráni svoju haldu (heap) globálnym zámkom a analýza hárka je náročná na alokácie: milióny buniek, variantov a WideStringov. Každé pracovné vlákno, ktoré sa dotkne haldy, čaká v rade na tento zámok, takže vlákna, ktoré v zdrojovom kóde vyzerajú nezávisle, bežia v praxi takmer po jednom. Náš prvy test to ukázal veľmi jasne. Na zošite s 8 hárkami s rozmermi 5 000 riadkov a 4 stĺpcov na hárok, meranom na i5-11600K (6 jadier, 12 vlákien) pod Win64, sa paralelné Open zlepšilo iba o 12 – 25 % oproti očakávaným minimálne 40 %. Prechod počtu vlákien cez 2, 3, 4, 6 a 8 vlákien vytvoril plochú krivku a v neskorších meraniach bola konfigurácia s 2 vláknami reálne o 26 % pomalšia ako sériová — klasický príznak dvoch vlákien bijúcich sa o sporný zámok
Tri merania potvrdili diagnózu a každé z nich vyvrátilo predchádzajúcu inštanciu. Po prvé, malý súbor (8 hárkov po 1 riadku) sa otvoril za 1,2 ms, čo dokazuje, že analýza predstavuje v podstate 100 % času Open a na vine nebol žiadny skrytý fixný náklad. Po druhé, mikrobenched čistej alokácie ukázal, že správca pamäte v Delphi škáluje spätne: rovnaký celkový objem 2 miliónov alokácií objektov a AnsiStringov bežal na 8 vláknach o 60 % pomalšie ako na jednom, zatiaľ čo rovnaký cyklus na halde WideStringov, čo je alokátor COM BSTR a nie Delphi MM, sa škáloval na 3,7-násobok. To, že HotXLS všade používa WideString, sa ukázalo ako zhoda okolností hrajúca v náš prospech. Po tretie, GetProcessTimes ukázal, že počas paralelného Open sa procesorový čas takmer rovnal reálnemu času: osem nominálnych vlákien spotrebovávalo výkon približne 1,3 vlákna. Pracovné vlákna nebežali naplno; spali v ceste sporu správcu pamäte, boli blokované a nie vyťažené
Praktické ponaučenie platí aj mimo tabuľkových procesorov. Ak je delphijská záťaž náročná na alokácie, zvýšenie počtu vlákien nič neprinesie, kým neklesne miera alokácie, a môže situáciu ľahko zhoršiť. Pred touto opravou sme používateľom ladiacim ParallelParseThreads hovorili úprimnú pravdu: pri súboroch limitovaných alokáciami prinieslo viac vlákien takmer nulový úžitok
Odkiaľ pochádza 20 alokácií haldy na jednu bunku?
Počítadlo alokácií nainštalované pomocou SetMemoryManager odpovedalo na túto otázku presne: približne 20 alokácií Delphi-MM na bunku, pričom 2,87 milióna z nich malo veľkosť 32 bajtov alebo menej. Vinníkom vôbec neboli objekty buniek. Metóda TXMLScaner.GetTokenValue vytvárala nový AnsiString pri každom volaní a volá sa približne 15 – 20-krát na bunku: raz pre názvy elementov, názvy atribútov, hodnoty atribútov a textový obsah. RTL cesta UTF8ToWideString navyše vytvárala dočasný medzikrok UnicodeString pre každú konverziu. Objekty buniek tvorili iba 160-tisíc alokácií, teda asi 8 % celku, čo náš pôvodný plán okamžite pochovalo: chceli sme vybudovať fond objektov buniek, ale čísla ukázali, že by sa to nikdy neoplatilo
Táto desaťminútová diagnostika stojí za vyskúšanie pri akomkoľvek vyšetrovaní výkonu v Delphi. Počítanie alokácií podľa veľkosti nestojí takmer nič a povie vám, odkiaľ tlak na správcu pamäte skutočne pramení — v našom prípade to boli dva návyky na úrovni RTL vo vnútri XML skenera a nie v objektovom modeli. Profilery stále ukazovali na parser ako celok; počítadlo ukázalo na dva konkrétne riadky
Oprava: internovanie tokenov a UTF-8 dekodér bez medzikrokov
Dve cielené zmeny v čítači XML odstránili viac ako polovicu alokácií na bunku bez toho, aby sme sa dotkli štruktúry parsera. Prvou je internovanie názvov elementov. XML hárka neustále opakuje malú slovnú zásobu: row, c, v, r, t, s a niekoľko názvov atribútov. Metóda InternTokenName udržiava 64-miestnu vyrovnávaciu pamäť predtým videných názvov a porovnáva buffer skenera s cache položkou pomocou TokenEqualsAnsi, čo je priame porovnanie bajtov bez alokácie. Pri zhode vráti nacachovaný AnsiString, pričom výber typu je dôležitý: AnsiString používa počítadlo referencií (refcount), takže vrátenie cache inštancie stojí len jedno zvýšenie refcountu a nulovú záťaž haldy. WideString nemá počítadlo referencií a každé priradenie prechádza cez SysAllocString, takže internovanie WideStringov by nič neušetrilo. Internovanie má zmysel robiť iba na type stringu s počítaním referencií
function TXMLScaner.InternTokenName: AnsiString;
var
Slot: Integer;
begin
Slot := TokenHash mod 64;
if TokenEqualsAnsi(FInternNames[Slot]) then
Result := FInternNames[Slot] // refcount++ only, no allocation
else
begin
Result := GetTokenValue; // materialize once, then cache
FInternNames[Slot] := Result;
end;
end;
Druhá zmena sa týka textu bunky. Stará cesta vytvárala token AnsiString, odovzdala ho UTF8ToWideString, ktorý vytvoril medzikrok UnicodeString, a ten bol nakoniec skonvertovaný na WideString, ktorý bunka ukladá: dve alokácie Delphi-MM na jeden textový token pred tým skutočným. Náhrada XmlUtf8ToWide(TokenPtr, TokenLen) je čistý pascalský dvojprechodový UTF-8 dekodér, ktorý číta priamo zo skenovacieho buffera: prvý prechod zmeria dĺžku UTF-16, druhý dešifruje do WideStringu alokovaného iba raz. Čisté náklady na textový token: jedna COM alokácia, nula Delphi-MM alokácií. Sémantická poznámka pre opatrných: pri chybnom formáte UTF-8 nový dekodér prepúšťa bajty bez toho, aby ich nahrádzal zástupnými znakmi, ako to robí RTL, čo ovplyvňuje len to, ako sa správajú poškodené súbory; pri platnom vstupe je výstup bajtovo identický. XML entity znakov sa k dekodéru nikdy nedostanú, pretože skener ich už vyriešil do UTF-8 v buffere tokenov
Čo sme tým získali a kde paralelná analýza stále nepomôže
Dve opravy znížili počet alokácií na bunku z približne 20 na 9,1 a čísla paralelného behu sa posunuli tak, ako predpovedala teória. Pri rovnakom teste s 8 hárkami a 5 000 riadkami na rovnakej zostave 6C12T sa zlepšenie na 8 vláknach posunulo zo 14 % na 47,4 %, čo predstavuje 1,90-násobné zrýchlenie oproti sériovému behu. Prípad s 2 vláknami prešiel z o 26 % pomalšieho na o 23,6 % rýchlejší a namerané využitie CPU stúplo z 1,0 na 2,2-násobok. Sériová cesta sa ako bonus zrýchlila o 3 %, keďže menej alokácií pomáha aj jednému vláknu. Zostávajúcich približne 9 alokácií na bunku tvoria zhruba z polovice objekty buniek a z polovice amortizovaný rast kontajnera; zmerali sme ich, vyhodnotili výnosy ako klesajúce a zastavili sa, pričom MM počítadlo je pripravené na ďalšie testovanie, ak to budúca záťaž odôvodní
Limity stojí za to uviesť rovnako jasne ako úspechy. HotXLS paralelizuje na úrovni hárkov, takže zošit, ktorý tvorí jeden obrovský hárok, sa analyzuje v jednom vlákne bez ohľadu na nastavenie ParallelParseThreads; pre tento prípad je lepším nástrojom streamovaný priamy čítač, pretože sa úplne vyhýba materializácii zošita. Súbory, ktorých čas sa spotrebúva v častiach fázy C (kresby, grafy a komentáre), vidia menší prínos, pretože táto fáza zostáva z návrhu sériová. Malé súbory nestoja za viacvláknový beh vôbec, preto dispečer pri nízkom počte úloh potichu zvolí sériový beh. A strop správcu pamäte nezmizol, len sa posunul vyššie: pri 9,1 alokáciách na bunku globálny zámok stále zaťažuje vlákna, preto osem vlákien prináša 1,90-násobné zrýchlenie a nie 4-násobné. Pre širšiu sadu nástrojov na zníženie časov načítania a ukladania, vrátane štýlov, fondov a hromadných spätných volaní, si pozrite nášho sprievodcu výkonom veľkých zošitov v Delphi
Paralelná analýza XLSX, vlastnosti ParallelParse a ParallelParseThreads a na alokácie nenáročný XML čítač opísaný v tomto článku sa dodávajú ako štandardné súčasti HotXLS Delphi Excel Component, ktorý natívne číta a zapisuje formáty XLS, XLSX a ODS v Delphi a C++Builder bez potreby automatizácie Excelu
Domov · Hľadať · losLab.com