HotPDF vykresluje mnoho stránek PDF souběžně přes dva vstupní body: RenderLoadedPagesParallel, který vrací pole bitmap, a RenderLoadedPagesParallelOrdered, který doručuje každou stránku do callbacku ve vstupním pořadí, jakmile je připravena. Uspořádaná forma je ta, která škáluje, protože nikdy nedrží v paměti více než omezený počet bitmap bez ohledu na to, kolik stránek jste požadovali
Tento rozdíl je celý smysl článku. Vykreslení 10 000 stránek při 300 DPI do pole znamená 10 000 živých bitmap, což není problém propustnosti, ale pád kvůli nedostatku paměti. Vykreslení přes uspořádaný callback s hloubkou fronty dva znamená dvě živé bitmapy a úloha běží v pevné paměťové stopě bez ohledu na to, jak dlouhý je dokument
Proč paralelní vykreslování potřebuje dvě fáze?
Vykreslení stránky PDF jsou ve skutečnosti dvě různé úlohy pod jedním názvem. Nejprve se obsahový stream tokenizuje a zkompiluje do zobrazovacího seznamu, což je vázáno na CPU a dotýká se sdílených keší. Pak se zobrazovací seznam přehraje do bitmapy, což je také vázáno na CPU, ale hodně alokuje. Zacházet s nimi jako s jednou nedělitelnou jednotkou nutí volit mezi serializací té poloviny, která se dotýká keší, a zdvojováním práce
HotPDF je rozděluje. Zámek keše zobrazovacích seznamů chrání pouze počítání frekvence, pořadí LRU, počty použití a rozhodnutí o přijetí; samotná kompilace běží mimo zámek. Po kompilaci si pracovník znovu dotáže keš: pokud mezitím jiný pracovník publikoval stejnou stránku, uvolní svou vlastní kopii a převezme počet použití publikované položky. To udržuje zdvojenou práci omezenou a zabraňuje dvěma položkám pro stejnou stránku, aniž by kdy serializovalo kompilaci napříč stránkami
Naměřený efekt na komplexním vzorku o 32 stránkách stojí za zapamatování. Celkový čas na Win64 klesl z 1 565 ms s jedním pracovníkem na 696 ms se čtyřmi, zhruba 2,25krát. Čas do prvního výsledku klesl z 1 255 ms na 63 ms, přibližně o 95 procent, protože pracovníci začnou přehrávat dokončené zobrazovací seznamy, zatímco jiní stále kompilují, místo aby čekali u plné kompilační bariéry
Uspořádané doručování a rezervovaný slot
Uspořádané API garantuje, že váš callback vidí stránky v pořadí, v jakém jste je požadovali, zatímco pracovníci dokončují v jakémkoli pořadí, v jakém skončí. Dokončené bitmapy se publikují do výstupního slotu indexovaného pozicí ve vstupu a callback běží sériově na volajícím vlákně
Dvě pravidla tomu dávají omezenost, nejen uspořádanost. Počet ve výstupní frontě zahrnuje bitmapy, které callback právě používá, takže pomalý callback nemůže nechat producenty proklouznout kolem hloubky fronty. A pozdější stránky mohou obsadit nejvýše queueDepth - 1 slotů, protože jeden slot je trvale rezervován pro další stránku, která je na řadě k doručení. Bez této rezervace může být pomalá první stránka zablokována dokončenými pozdějšími stránkami, které zaplní frontu, a pipeline se zablokuje na čele fronty s každým pracovníkem nečinným
uses
HPDFDoc;
procedure TExportJob.PageReady(Sender: TObject; InputIndex,
PageIndex: Integer; Bitmap: TBitmap; var Cancel: boolean);
begin
// Bitmapa je vypůjčená: platná pouze pro toto volání. Spotřebujte ji zde
// (zapište na disk, zakódujte, hašujte) a neuchovávejte odkaz
Bitmap.SaveToFile(Format('page-%.4d.bmp', [InputIndex + 1]));
Cancel := FUserCancelled;
end;
procedure TExportJob.Run;
var
Pdf: THotPDF;
Pages: array of Integer;
Info: THPDFParallelRenderPipelineInfo;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('manual.pdf') <= 0 then
Exit;
SetLength(Pages, Pdf.LoadedPageCount);
for I := 0 to High(Pages) do
Pages[I] := I;
Pdf.RenderLoadedPagesParallelOrdered(Pages, 200, 4, 2,
PageReady, Info);
Writeln(Format('workers=%d peak queued=%d peak bytes=%d',
[Info.WorkerCount, Info.PeakQueuedPageCount, Info.PeakQueuedBytes]));
Writeln(Format('first result=%d ms total=%d ms backpressure waits=%d',
[Info.FirstResultMilliseconds, Info.TotalMilliseconds,
Info.BackpressureWaitCount]));
finally
Pdf.Free;
end;
end;
Smlouva o vypůjčené bitmapě je důvodem, proč paměťová stopa zůstává plochá. Knihovna uvolní bitmapu, jakmile se váš callback vrátí, takže vzorek o 10 000 stránkách s hloubkou fronty 2 dosahuje vrcholu přesně dvou bitmap na Win32 i Win64. Pokud bitmapu potřebujete i po callbacku, zkopírujte ji, a pak vlastníte paměť i účtování vy
Jak paměťový rozpočet vybírá počet pracovníků
Více pracovníků není vždy rychlejší a často je to fatální. Šest pracovníků vykreslujících stránky A3 při 600 DPI potřebuje několik set megabajtů živé bitmapy bez ohledu na to, kolik jader je k dispozici, a stroj s osmi jádry nemusí mít tolik volného adresního prostoru
HotPDF proto normalizuje požadovaný počet pracovníků proti velikosti požadavku, počtu stránek a pevnému stropu a pak dělí aktuálně dostupný globální rozpočet nejkonzervativnějším odhadem jedné stránky v požadavku. Alespoň jeden pracovník vždy přežije, takže i příliš velká stránka se vykreslí, byť samostatně. V referenčním měření běžela šestistránková úloha při 600 DPI pod rozpočtem 512 MiB se třemi pracovníky místo šesti, s vrcholem rezervace 504 583 296 bajtů
Každý pracovník provede rezervaci paměti poté, co si zabere stránku a před kompilací, a rezervace pokrývá kompilaci i přehrávání dohromady. Rezervace jen kolem rastrovací fáze by přehlédla zobrazovací seznam a pracovní sadu obsahového streamu, což je na graficky náročných stránkách větší polovina. Dvě API se pak liší: pole uvolní svou rezervaci, jakmile je bitmapa hotová, protože volající vlastní výsledek a plánovač ho už nemůže sledovat, zatímco uspořádaná forma přenese rezervaci spolu s bitmapou do výstupního slotu a drží ji, dokud se callback nevrátí. To je další důvod, proč preferovat uspořádané API, když je výstupní sada velká
Zrušení bez deadlocku
Nastavení Cancel v callbacku zastaví novou práci a probudí každého pracovníka čekajícího na zpětný tlak. Stránky již uvnitř rendereru se dokončí a uklidí, místo aby byly opuštěny, a uchová se jen první diagnostika selhání, vyvolaná poté, co se připojí každý pracovník
Pořadí je zde nosné. Stav zrušení se nastaví jako první, nepublikované výstupní rezervace se uvolní jako druhé a teprve pak se pracovníci připojí. Prohození posledních dvou kroků vytváří uzavřenou smyčku: pracovník čeká na přijetí do rozpočtu, který se neuvolní, dokud spotřebitel neskončí, zatímco spotřebitel čeká, až tento pracovník skončí
Interaktivní aplikace by měly znát i preempci na popředí. Předběžné načítání stránek běží na vlastních tokenech zrušení a volání vykreslování na popředí zruší a připojí pracovníky předběžného načítání dřív, než provedou vlastní práci, takže propustnost na pozadí nikdy nepřidá latenci ke stránce, na kterou uživatel čeká. Zrušení se kontroluje v intervalech omezených 4 KiB skenování bajtů nebo 256 tokeny, ačkoli atomické volání kodeku nebo operačního systému nelze přerušit uprostřed běhu — záruka odezvy pokrývá smyčky vlastněné knihovnou, ne dekodéry třetích stran. Přístup interaktivního vykreslování založený na frontě popisuje článek vykreslování na pozadí s frontou požadavků
Čtení statistik pipeline
THPDFParallelRenderPipelineInfo odděluje čísla, která se snadno pletou. RequestedWorkerCount proti WorkerCount a MemoryBudgetWorkerLimit vám řekne, zda rozpočet snížil vaši souběžnost. CompiledPageCount proti CacheHitPageCount vám řekne, kolik ušetřila keš zobrazovacích seznamů. CompiledPagesAtFirstResult ukazuje, zda se pipeline opravdu překrývala v kompilaci a přehrávání, nebo degenerovala na bariéru
Pro ladění jsou nejužitečnější dvě hodnoty: BackpressureWaitCount a MemorySchedulerWaitMilliseconds. Vysoký počet čekání na zpětný tlak znamená, že úzkým hrdlem je váš callback, takže ho buď zrychlete, nebo zvyšte hloubku fronty, pokud to paměť dovolí. Vysoké čekání plánovače znamená, že úzkým hrdlem je rozpočet, takže snižte DPI, snižte souběžnost, nebo zvyšte rozpočet. Zvýšení počtu pracovníků v obou případech propustnost zhorší, což je ta neintuitivní část
Praktický výchozí bod pro dávkový export: souběžnost rovná fyzickému počtu jader minus jedna, hloubka fronty dvě nebo tři a DPI zvolené podle skutečného požadavku na výstup, ne ze zvyku. Pak si přečtěte statistiky z reálného dokumentu a upravte jednou, místo abyste hádali dvakrát. Pro úlohy, kde je samotný dokument příliš velký na to, aby se vešel do paměti, kombinujte toto se streamovacím modelem přístupu popsaným v článku přímé souborové API pro workflow s velkými PDF
Paralelní vykreslování, předběžné načítání a keš zobrazovacích seznamů jsou součástí stejného vykreslovacího stacku pro Delphi a C++Builder; úplný seznam funkcí najdete na stránce komponenty HotPDF Delphi PDF component