Technický článek

Poctivé PDF load/save benchmarky v Delphi: brány proti šumu

Poctivý load/save benchmark pro PDF Library for Delphi měří LoadFromFile a SaveToFile přes QueryPerformanceCounter, drží syrové ticky a frekvenci čítače, pouští baseline a kandidáta ve střídavých párech A/B, B/A, A/B, odmítá startovat, když zátěž CPU sedí nad 25 %, odmítá jakýkoli výsledek, jehož rozpětí range-to-median přesáhne 15 %, a vyhazuje každé měření, jehož uložené PDF neprojde strukturální, renderovací ani sémantickou validací. Tenhle seznam působí byrokraticky, dokud se vám poprvé tvrzení „20 % rychleji“ nevypaří při opakovaném běhu. Následuje, jak se k tomu dedikovaná corpus sonda a její comparison runner dostaly, včetně běhu, kdy byla mašina prostě moc zaneprázdněná na to, aby se dalo cokoli změřit, a harness to správně řekl

Proč PDF benchmark v Delphi hlásí nulové sekundy?

PDF load benchmark hlásí nulové sekundy, když jeho hodiny tikají hruběji, než jak rychlá je měřená operace, a GetTickCount64 je přesně tenhle druh hodin: vrací milisekundy, ale na Windows postupuje jen když se spustí přerušení systémového časovače, obvykle každých 15,6 ms. FPC port demo benchmarku velkých souborů v PDF Library for Delphi ho použil, protože TStopwatch v té toolchain není k dispozici, a zaznamenává uplynulý čas na tři desetinná místa. Načtení malé CAD kresby nebo krátkého tagged dokumentu skončí pohodlně uvnitř jednoho kroku časovače, takže demo občas vytisklo 0.000 pro načtení, které zjevně dělalo skutečnou práci

function ElapsedSeconds(StartTick: QWord): Double;
begin
  Result:= (GetTickCount64- StartTick)/ 1000.0;
end;

// uvnitř smyčky operace
Lib:= TPDFlib.Create;
try
  Lib.OnProgress:= Reporter.Progress;
  Started:= GetTickCount64;
  LoadCode:= Lib.LoadFromFile(InputFile, Password);
  ...

Nula je horší než nepřesné číslo, protože každé srovnání, které na ní postavíte, dělí nulou. Párovaný comparison runner bere jakoukoli větev s nulovým minimem jako neprůkaznou s odůvodněním „Zero duration prevents a meaningful ratio“, což je správné odmítnutí, ale znamená to taky, že demo měření zanechalo díru přesně tam, kde žijí krátké soubory. Tentýž demo si instaluje i callback OnProgress, takže jeho časy zahrnují režii callbacku, kterou by čisté load/save měření nosit nemělo, a archivovaná demo čísla nejsou zaměnitelná s čímkoli, co změříte později

Měření LoadFromFile a SaveToFile přes QueryPerformanceCounter

Dedikovaná konzolová sonda Tests/CorpusLoadSave.dpr měří na každý vstupní soubor dvě operace přes QueryPerformanceCounter: LoadFromFile plus čtení PageCount a LoadFromFile plus PageCount plus SaveToFile. Každá operace dostane čerstvou instanci TPDFlib a žádný progress callback a konstruktor a destruktor instance sedí mimo měřenou oblast, stejně jako zápis CSV a veškerá validace výstupu. Čítač se čte bezprostředně před načtením a bezprostředně po posledním volání knihovny a LastErrorCode se tahá až po druhém čtení

Měřená oblast corpus sondy PDFlibPas: QueryPerformanceCounter se čte bezprostředně před LoadFromFile a znovu po posledním volání knihovny, s PageCount a SaveToFile uvnitř, zatímco sestavení instance, zápis CSV, validace výstupu a tahání chybového kódu zůstávají mimo měřenou oblast
Syrové ticky a frekvence čítače se zaznamenávají vedle odvozených sekund, takže CAD kresba načtená za 8 888 ticků při deseti milionech ticků za sekundu se zachová jako reálná data místo zaokrouhlení na nulu
Lib:= TPDFlib.Create;
try
  if not QueryPerformanceCounter(Started) then
    raise Exception.Create('Performance counter unavailable');
  Code:= Lib.LoadFromFile(WideString(SourceFile), '');
  if Code= 1 then
  begin
    Pages:= Lib.PageCount;
    if Save then Code:= Lib.SaveToFile(WideString(SavedFile));
  end;
  if not QueryPerformanceCounter(Finished) then
    raise Exception.Create('Performance counter unavailable');
  ErrorCode:= Lib.LastErrorCode;
finally
  Lib.Free;
end;
Ticks:= Finished- Started;
if Ticks< 0 then
  raise Exception.Create('Performance counter moved backwards');

Sonda zapisuje syrový počet ticků a frekvenci čítače vedle odvozených sekund, formátovaných na devět desetinných míst s pevným desetinným oddělovačem ., takže si kdokoli může podíl z CSV přepočítat, místo aby mu věřil. Na FPC Win64 buildu se vzorek CAD načetl za 8 888 ticků při 10 000 000 ticků za sekundu, zapsáno jako 0.000888800 sekundy — pozorování, které by starý časovač zaokrouhlil na nulu. Sonda záměrně nezastřihává krátké hodnoty, nenahrazuje je minimální dobou ani neodečítá odhadnutou režii časovače a i když volání knihovny selže, zapíše oba řádky a skončí s nenulovým exit kódem. Devět číslic ale není přesnost: víc zaznamenaných desetinných míst neříká nic o opakovatelnosti a zašuměná či nulová pozorování je pořád potřeba odmítnout na výstupu

if not QueryPerformanceFrequency(Frequency) or (Frequency<= 0) then
  raise Exception.Create('Performance counter frequency unavailable');
NumberFormat:= TFormatSettings.Create;
NumberFormat.DecimalSeparator:= '.';
...
Rows.Add(CSV(ExtractFileName(SourceFile))+ ','+ CSV(Operation)+ ','+
  FormatFloat('0.000000000', Ticks/ Frequency, NumberFormat)+ ','+
  IntToStr(Code)+ ','+ IntToStr(ErrorCode)+ ','+ IntToStr(Pages)+ ','+
  IntToStr(Ticks)+ ','+ IntToStr(Frequency));

Co dělá srovnání časů PDF load/save důvěryhodným?

Srovnání časů mezi dvěma buildy PDF Library for Delphi je důvěryhodné jen tehdy, když jsou pořadí startu, podmínky startu i rozptyl všechny kontrolované a zaznamenané, takže comparison runner naplánuje nejméně tři páry v pořadí A/B, B/A, A/B. Když se baseline pouští vždy první, tichou rukou předá kandidátovi teplejší file cache a jiný teplotní stav; střídání pořadí rozprostře tenhle bias přes obě větve místo připisování jedné. Před každou větví runner spočítá SHA-256 kompletního vstupního souboru, což ověří, že se nic nezměnilo, a zároveň předčte tytéž bajty pro obě větve, a po každém běhu znovu hashuje oba spustitelné soubory i validační nástroje, takže znovu sestavený binary se nemůže vetřít doprostřed série

Runner pak jednou za sekundu vzorkuje celostrojové vytížení CPU a větev spustí jen když vzorek spadne na 25 % a níž, s maximální čekací dobou 30 sekund, než pokus zaznamená jako odmítnutý. Tahle brána kontroluje podmínku startu a nic víc: neizoluje mašinu během běhu a stav napájení, termální throttling, práce na pozadí i caching operačního systému můžou čísla pořád hýbat. Druhý filtr je proto statistický v tom nejjednodušším smyslu. Pro každou operaci runner spočítá range dělený median pro větev baseline, větev kandidáta a rozdělení párovaných poměrů kandidát/baseline, a když kterýkoli ze tří přesáhne 0.15, výsledek se označí jako noisy místo reportování jako zjištění

Brány párovaného srovnání v PDFlibPas: tři páry běží v pořadí A/B, B/A, A/B s SHA-256 hashováním vstupu před každou větví, startovní brána čeká na CPU na 25 procentech a níž a rozpětí range-to-median nad 0.15 na LoadFromFile nebo větvi load-plus-SaveToFile označí běh jako noisy
Střídání pořadí startu rozprostře cache i teplotní bias přes obě větve a same-binary kontrola ukazuje, co takové uspořádání dokáže: poměry blízko 1.0 prokazují opakovatelnost, nikdy tvrzení o zrychlení

Proč same-binary kontrola prokazuje opakovatelnost, ne rychlost?

Same-binary kontrola pouští identické spustitelné soubory jako baseline i kandidáta, takže poměr blízko 1.0 může prokázat jen to, že se měřicí uspořádání opakuje samo; nikdy neukáže, že implementace zrychlila. První přísná kontrola 2026-09-21 použila high-resolution FPC Win64 sondu proti přijaté tagged příručce o 70 stránkách a všech šest startů se odmítlo, protože vzorky CPU se pohybovaly od 26,5 % do 93,8 %. Report obsahoval selhání a žádné agregáty, což je přesně ten výstup, který chcete, když je mašina zaneprázdněná. Opakování v týž den s bajtově identickými vstupy, týmž probe spustitelným souborem a nezměněnými prahy přijalo všech šest startů do 3 sekund; každé rozpětí range-to-median skončilo mezi 0.019 a 0.054 a mediány poměrů byly 1.0084 pro LoadFromFile a 0.9872 pro LoadFromFile + SaveToFile

Ta dvojice čísel zakládá kvalifikované pozorovací okno a nic víc. Když se oba binary liší, stabilní běh se označí jako deskriptivní srovnání s explicitní poznámkou, že poměry jsou pozorování, ne statistická významnost ani tvrzení o zrychlení. Ta disciplína hraje největší roli, když validujete cílené optimalizace, jako jsou ty popsané v článku o profilování PDF Library for Delphi a nahrazování hot paths hash indexy: profiler vám řekne, kam čas teče, ale jen kontrolovaný párovaný běh na reálných dokumentech řekne, jestli změna přežila kontakt s celým pipeline. Ještě jedna hranice, kterou stojí za to vyslovit nahlas — normal-save zahrnuje načítání a peak working set, který runner zaznamenává, je celoprocessní, takže žádná z té paměti se nedá připsat samotnému ukládání

Tři výstupní brány a matice čtyř kompilátorů

Žádný čas PDF Library for Delphi se nepočítá, dokud soubor, který vytvořila, neprojde třemi nezávislými branami, protože ukládání, které rychle napíše rozbité PDF, není rychlejší ukládání. Benchmark nejdřív zkontroluje, že obě operace vrátily 1 a nahlásily přijatý počet stránek, a pak validuje jediné uložené PDF v tomhle pořadí:

Tři výstupní brány v PDFlibPas: obě operace musí vrátit 1 s přijatým PageCount, nezávislý checker musí projít uložený soubor bez varování, každá stránka se musí vyrenderovat do sady SHA-256 obrázků na stránku odpovídající zdroji a neverbální sémantika musí sedět na optional content a měřicích strukturách
Ukládání, které rychle napíše rozbité PDF, není rychlejší ukládání, takže se čas počítá jen když struktura, renderování i neverbální sémantika souhlasí, že výstup je pořád tentýž dokument
  • Struktura: nezávislý PDF checker musí projít uložený soubor bez chyb ani varování
  • Renderování: každá stránka se vyrenderuje ve svém defaultním stavu a sada SHA-256 obrázků na stránku musí přesně sedět na referenční render přijatého zdroje
  • Neverbální sémantika: samostatné sémantické srovnání se zdrojem pokrývá vybrané vlastnosti, které pixely neukážou, včetně optional-content a měřicích struktur v rámci jejich zdokumentovaného rozsahu

S těmito branami proběhla plná lokální corpus matice na FPC Win32, FPC Win64, Delphi Win32 a Delphi Win64 přes 12 přijatých PDF s 1 612 zdrojovými stránkami, což dalo 48 párů sample/target a 6 448 validovaných výstupních stránek bez žádných vybraných sémantických rozdílů. Všech 96 měření operací si podrželo kladné syrové hodnoty čítače konzistentní s hlášenými sekundami a tyhle hodnoty se záměrně neagregují do tabulky rychlosti napříč kompilátory, protože matice je funkční důkaz, ne kontrolované srovnání. Load/save cesta taky netvrdí, že dekóduje každý embedded obrázek, validuje podpisy, spouští XFA nebo certifikuje PDF/UA; pokud potřebujete soudit renderovací throughput místo nákladů load/save, lepším startem jsou concurrency omezení popsaná v článku o paralelním renderování stránek a thread safety v PDF Library for Delphi

Praktické ponaučení je krátké: držte syrové čítače, střídejte pořadí, bráňte start, odmítejte zašuměná rozpětí a nikdy neměřte výstup, který jste nevalidovali. Ta pravidla jsou to, co PDF Library for Delphi dovoluje říct „žádná měřitelná změna“ se stejnou jistotou jako „rychlejší“, a týž probe zdroj se nezměněný kompiluje na Delphi i FPC pro Win32 a Win64. Knihovnu, její load/save API a podporované kompilátory si můžete projít na stránce produktu PDF Library for Delphi