HotPDF renderar många PDF-sidor samtidigt via två ingångspunkter: RenderLoadedPagesParallel, som returnerar en array av bitmappar, och RenderLoadedPagesParallelOrdered, som levererar varje sida till en callback i indataordning så snart den är klar. Den ordnade formen är den som skalar, eftersom den aldrig håller fler än ett begränsat antal bitmappar i minnet oavsett hur många sidor du bad om
Skillnaden är hela artikeln. Att rendera 10 000 sidor vid 300 DPI till en array betyder 10 000 levande bitmappar, vilket inte är ett genomströmningsproblem utan en krasch på grund av minnesbrist. Att rendera dem via en ordnad callback med ett ködjup på två betyder två levande bitmappar, och jobbet körs med ett fast minnesavtryck oavsett hur långt dokumentet är
Varför behöver parallell rendering två stadier?
Att rendera en PDF-sida är egentligen två olika jobb under samma namn. Först tokeniseras och kompileras innehållsströmmen till en visningslista, vilket är CPU-bundet och rör vid delade cachar. Sedan spelas visningslistan upp till en bitmapp, vilket också är CPU-bundet men allokerar tungt. Att behandla dem som en odelbar enhet tvingar fram ett val mellan att serialisera den cacherörande halvan och att duplicera arbete
HotPDF delar upp dem. Låset för visningslistecachen skyddar bara frekvensräkningen, LRU-ordningen, användningsräknarna och tillträdesbesluten; själva kompileringen körs utanför låset. Efter kompileringen frågar arbetaren cachen igen: om en annan arbetare publicerade samma sida under tiden släpper den sin egen kopia och tar en användningsräknare på den publicerade posten. Det håller dubbelarbetet begränsat och förhindrar två poster för samma sida, utan att någonsin serialisera kompilering över sidor
Den uppmätta effekten på en komplex 32-sidig testfixtur är det värt att komma ihåg. Totaltiden på Win64 sjönk från 1 565 ms med en arbetare till 696 ms med fyra, ungefär 2,25 gånger. Tiden till det första resultatet sjönk från 1 255 ms till 63 ms, cirka 95 procent, eftersom arbetare börjar spela upp färdiga visningslistor medan andra fortfarande kompilerar istället för att vänta vid en fullständig kompileringsbarriär
Ordnad leverans och den reserverade platsen
Det ordnade API:et garanterar att din callback ser sidor i den ordning du begärde dem, medan arbetare blir klara i vilken ordning de nu blir det. Färdiga bitmappar publiceras till en utdataplats indexerad efter indataposition, och callbacken körs seriellt på den anropande tråden
Två regler gör detta begränsat snarare än bara ordnat. Utdataköns antal inkluderar bitmappar som för närvarande används av callbacken, så en långsam callback kan inte låta producenter smyga förbi ködjupet. Och senare sidor får uppta högst queueDepth - 1 platser, eftersom en plats permanent är reserverad för nästa sida som ska levereras. Utan den reservationen kan en långsam första sida låsas ute av färdiga senare sidor som fyller kön, och pipelinen når dödläge i kösens huvud medan varje arbetare är sysslolös
uses
HPDFDoc;
procedure TExportJob.PageReady(Sender: TObject; InputIndex,
PageIndex: Integer; Bitmap: TBitmap; var Cancel: boolean);
begin
// Bitmappen är lånad: giltig endast för detta anrop. Konsumera den
// här (skriv till disk, koda, hasha) och behåll inte referensen
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;
Kontraktet med lånade bitmappar är anledningen till att minnesavtrycket förblir platt. Biblioteket släpper bitmappen så snart din callback returnerar, så en testfixtur på 10 000 sidor vid ködjup 2 når som mest exakt två bitmappar på både Win32 och Win64. Om du behöver bitmappen efter callbacken, kopiera den, och då äger du minnet och bokföringen
Hur minnesbudgeten väljer ett antal arbetare
Fler arbetare är inte alltid snabbare och är ofta ödesdigert. Sex arbetare som renderar A3-sidor vid 600 DPI behöver flera hundra megabyte levande bitmapp oavsett hur många kärnor som är tillgängliga, och maskinen som har åtta kärnor kanske inte har så mycket ledigt adressutrymme
HotPDF normaliserar därför det begärda antalet arbetare mot förfrågans storlek, sidantalet och ett hårt tak, och delar sedan den aktuella tillgängliga globala budgeten med det största konservativa enkelsidesestimat i förfrågan. Minst en arbetare överlever alltid, så en överdimensionerad sida renderas fortfarande, dock ensam. I referensmätningen körde ett sexsidigt 600 DPI-jobb under en 512 MiB-budget med tre arbetare istället för sex, med en reservationstopp på 504 583 296 byte
Varje arbetare gör en minnesreservation efter att ha tagit en sida och innan kompilering, och reservationen täcker kompilering och uppspelning tillsammans. Att bara reservera runt rasterstadiet skulle missa visningslistan och innehållsströmmens arbetsmängd, vilket på grafiktunga sidor är den större halvan. De två API:erna skiljer sig sedan åt: arrayformen släpper sin reservation så snart bitmappen är klar, eftersom anroparen äger resultatet och schemaläggaren inte längre kan bokföra det, medan den ordnade formen för över reservationen tillsammans med bitmappen in i utdataplatsen och håller den tills callbacken returnerar. Det är ännu ett skäl att föredra det ordnade API:et när utdatamängden är stor
Avbrytning utan dödläge
Att sätta Cancel i callbacken stoppar nytt arbete och väcker varje arbetare som väntar på mottryck. Sidor redan inne i renderaren blir klara och städas upp istället för att överges, och endast den första feldiagnosen behålls, kastad efter att varje arbetare har återförenats
Ordningen här är bärande. Avbrytningstillståndet sätts först, opublicerade utdatareservationer släpps för det andra, och först då återförenas arbetarna. Att kasta om de sista två stegen skapar en sluten loop: en arbetare väntar på tillträde till en budget som inte släpps förrän konsumenten är klar, medan konsumenten väntar på att den arbetaren ska avsluta
Interaktiva applikationer bör också känna till förgrundsförträngning. Sidförhämtning körs på sina egna avbrytningstokens och förgrundsrenderingsanropen avbryter och återförenar förhämtningsarbetarna innan de gör sitt eget arbete, så att bakgrundsgenomströmning aldrig lägger till latens till sidan en användare väntar på. Avbrytning kontrolleras vid intervall begränsade till 4 KiB byteskanning eller 256 tokens, även om en atomär kodek eller ett OS-anrop inte kan avbrytas mitt i flödet — responsgarantin täcker bibliotekets egna loopar, inte tredjepartsavkodare. Den köbaserade metoden för interaktiv rendering beskrivs i bakgrundsrendering med en förfrågningskö
Att läsa pipelinestatistiken
THPDFParallelRenderPipelineInfo separerar siffror som lätt blandas ihop. RequestedWorkerCount mot WorkerCount och MemoryBudgetWorkerLimit visar om budgeten begränsade din samtidighet. CompiledPageCount mot CacheHitPageCount visar hur mycket visningslistecachen sparade. CompiledPagesAtFirstResult visar om pipelinen verkligen överlappade kompilering och uppspelning eller degenererade till en barriär
För finjustering är de två mest användbara BackpressureWaitCount och MemorySchedulerWaitMilliseconds. Höga mottrycksväntetider betyder att din callback är flaskhalsen, så gör den antingen snabbare eller höj ködjupet om minnet tillåter. Höga schemaläggarväntetider betyder att budgeten är flaskhalsen, så sänk DPI, sänk samtidigheten, eller höj budgeten. Att höja antalet arbetare i endera fallet gör genomströmningen sämre, vilket är den kontraintuitiva delen
En praktisk startpunkt för batch-export: samtidighet lika med fysiska kärnor minus en, ködjup två eller tre, och DPI valt utifrån det faktiska utdatakravet snarare än av vana. Läs sedan statistiken från ett verkligt dokument och justera en gång, snarare än att gissa två gånger. För jobb där dokumentet i sig är för stort för att hålla i minnet, kombinera detta med strömningsåtkomstmodellen i det direkta fil-API:et för stora PDF-arbetsflöden
Parallell rendering, förhämtning och visningslistecachen är alla delar av samma renderingsstack för Delphi och C++Builder; den fullständiga funktionslistan finns på HotPDF Delphi PDF-komponentsidan