Teknisk artikel

Ærlige PDF load/save-benchmarks i Delphi: støjgates

Et ærligt load/save-benchmark for PDF Library for Delphi tider LoadFromFile og SaveToFile med QueryPerformanceCounter, gemmer de rå ticks og counter-frekvensen, kører baseline og kandidat i alternerende A/B-, B/A-, A/B-par, nægter at starte, mens CPU-belastningen ligger over 25%, afviser ethvert resultat, hvis range-to-median-spread overstiger 15%, og smider enhver timing væk, hvis gemte PDF ikke består strukturel, renderings- eller semantisk validering. Listen lyder som bureaukrati, indtil første gang et "20% hurtigere"-claim fordamper ved en genkørsel. Det følgende er, hvordan den dedikerede corpus-probe og dens comparison-runner kom dertil, inklusive den kørsel, hvor maskinen simpelthen var for optaget til at måle noget, og harnessen korrekt sagde netop det

Hvorfor rapporterer et Delphi PDF-benchmark nul sekunder?

Et PDF-load-benchmark rapporterer nul sekunder, når dets ur tick'er mere groft end den operation, det måler, og GetTickCount64 er præcis dén slags ur: det returnerer millisekunder, men på Windows skrider det kun frem, når systemets timer-interrupt udløses, typisk hver 15,6 ms. FPC-porten af huge-file-benchmark-demoen i PDF Library for Delphi brugte den, fordi TStopwatch ikke er tilgængelig i den toolchain, og den registrerer forløbet tid med tre decimaler. Indlæsning af en lille CAD-tegning eller et kort tagget dokument afsluttes godt inden for ét timer-trin, så demoen udskrev nogle gange 0.000 for en indlæsning, som klart gjorde rigtigt arbejde

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

// inde i operationsløkken
Lib:= TPDFlib.Create;
try
  Lib.OnProgress:= Reporter.Progress;
  Started:= GetTickCount64;
  LoadCode:= Lib.LoadFromFile(InputFile, Password);
  ...

Et nul er værre end et upræcist tal, fordi hver sammenligning, du bygger på det, dividerer med det. Den parrede comparison-runner behandler enhver arm med et nul-minimum som inkonklusiv med begrundelsen "nul varighed forhindrer et meningsfuldt forholdstal", hvilket er den korrekte nægtelse, men det betyder også, at demo-timingerne efterlod et målehul præcis der, hvor de korte filer lever. Samme demo installerer desuden en OnProgress-callback, så dens timings inkluderer callback-overhead, som en ren load/save-måling ikke bør bære, og arkiverede demo-tal er ikke udskiftelige med noget, der er målt senere

Timing af LoadFromFile og SaveToFile med QueryPerformanceCounter

Den dedikerede console-probe, Tests/CorpusLoadSave.dpr, måler to operationer pr. inputfil med QueryPerformanceCounter: LoadFromFile plus læsning af PageCount, og LoadFromFile plus PageCount plus SaveToFile. Hver operation får en frisk TPDFlib-instans og ingen progress-callback, og instansens constructor og destructor står uden for det timede område, ligesom CSV-skrivning og al output-validering. Counteren læses umiddelbart før indlæsningen og umiddelbart efter det sidste bibliotekskald, og LastErrorCode hentes først efter den anden aflæsning

Det timede område i PDFlibPas corpus-proben: QueryPerformanceCounter læses umiddelbart før LoadFromFile og igen efter det sidste bibliotekskald, med PageCount og SaveToFile indeni, mens instansopsætning, CSV-skrivning, output-validering og hentning af fejlkoden alle bliver uden for det timede område
Rå ticks og counter-frekvensen gemmes ved siden af de afledte sekunder, så en CAD-tegning, der loader på 8.888 ticks ved ti millioner ticks i sekundet, bevares som rigtige data i stedet for at blive rundet til nul
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');

Proben skriver det rå tick-antal og counter-frekvensen ved siden af de afledte sekunder, formateret med ni decimaler og en fast . som decimalseparator, så enhver kan genberegne kvotienten fra CSV-filen i stedet for at stole på den. På FPC Win64-builden loadede CAD-eksemplet på 8.888 ticks ved 10.000.000 ticks i sekundet, registreret som 0.000888800 sekunder — en observation, som det gamle ur ville have rundet til nul. Proben klipper bevidst ikke korte værdier, substituerer ikke en minimumsvarighed og trækker ikke et estimeret timer-overhead fra, og den skriver stadig begge rækker med en ikke-nul exitkode, når et bibliotekskald fejler. Ni cifre er dog ikke det samme som accuracy: mere registreret præcision siger intet om reproducerbarhed, og støjende eller nul-observationer skal stadig afvises downstream

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));

Hvad gør en PDF load/save-timingssammenligning troværdig?

En timingssammenligning mellem to builds af PDF Library for Delphi er kun troværdig, når startrækkefølge, startbetingelser og spread alle er kontrolleret og registreret, så comparison-runneren planlægger mindst tre par i A/B-, B/A-, A/B-rækkefølge. Altid at køre baselinen først giver i stilhed kandidaten en varmere file-cache og en anden termisk tilstand; at alterne rækkefølgen fordeler den bias på begge arme i stedet for at kreditere den til én. Før hver arm hasher runneren den komplette inputfil med SHA-256, hvilket både verificerer, at intet er ændret, og pre-læser de samme bytes til hver af armen, og den hasher de to eksekverbare filer og valideringsværktøjerne igen efter hver kørsel, så en rekompileret binary ikke kan snige sig ind midt i en serie

Runneren sampler derefter maskinomfattende CPU-udnyttelse én gang i sekundet og starter armen kun, når en sample falder til 25% eller derunder, med højest 30 sekunders ventetid, før forsøget registreres som afvist. Den gate styrer startbetingelsen og intet andet: den isolerer ikke maskinen under kørslen, og strømtilstand, termisk throttling, baggrundsarbejde og OS-caching kan stadig flytte tallene. Derfor er det andet filter statistisk i sin mest bogstavelige forstand. For hver operation beregner runneren range divideret med median for baseline-armen, kandidat-armen og fordelingen af parrede kandidat/baseline-ratioer, og hvis en af de tre overstiger 0,15, mærkes resultatet som noisy i stedet for at blive rapporteret som et fund

Parrede comparison-gates i PDFlibPas: tre par køres i A/B-, B/A-, A/B-rækkefølge med SHA-256-hashing af input før hver arm, en startgate venter på CPU på eller under 25 procent, og range-to-median-spreads over 0,15 på LoadFromFile eller load-plus-SaveToFile-armen mærker kørslen som noisy
Alternering af startrækkefølgen fordeler cache- og termisk bias på begge arme, og same-binary-kontrollen viser, hvad sådan et setup kan bevise: ratioer nær 1,0 etablerer gentagelighed, aldrig et speedup-claim

Hvorfor beviser en same-binary-kontrol gentagelighed, ikke hastighed?

En same-binary-kontrol kører identiske eksekverbare filer som baseline og kandidat, så en ratio nær 1,0 kan kun bevise, at målesetuppet gentager sig selv; den kan aldrig vise, at en implementering blev hurtigere. Den første strikse kontrol 2026-09-21 brugte high-resolution FPC Win64-proben mod en godkendt tagget guide på 70 sider, og alle seks starts blev afvist, fordi CPU-samplerne lå mellem 26,5% og 93,8%. Rapporten indeholdt fejl og ingen aggregater, hvilket præcis er det udfald, du ønsker, når maskinen er optaget. Et genforsøg samme dag med byte-identiske inputs, samme probe-eksekverbare og uændrede tærskler accepterede alle seks starts inden for 3 sekunder; ethvert range-to-median-spread landede mellem 0,019 og 0,054, og ratio-medianerne var 1,0084 for LoadFromFile og 0,9872 for LoadFromFile + SaveToFile

Det talpar etablerer et kvalificeret observationsvindue og ikke mere. Når de to binaries adskiller sig, mærkes en stabil kørsel som en deskriptiv sammenligning, med den eksplicitte note, at ratioerne er observationer, ikke statistisk signifikans eller et speedup-claim. Disciplinen betyder mest, når du validerer målrettede optimeringer som dem, der beskrives i profiling af PDF Library for Delphi og udskiftning af hot paths med hash-indekser: en profiler fortæller dig, hvor tiden går, men kun en kontrolleret parret kørsel på rigtige dokumenter fortæller dig, om ændringen overlevede kontakten med hele pipelinen. Én grænse mere, der er værd at sige højt — normal-save inkluderer indlæsning, og det peak working set, runneren registrerer, er procesomfattende, så ingen af det er hukommelse, der alene kan tilskrives gemningen

Tre output-gates og en fire-compiler-matrix

Ingen PDF Library for Delphi-timing tæller, medmindre filen, den producerede, består tre uafhængige gates, fordi en gemning, der hurtigt skriver en ødelagt PDF, ikke er en hurtigere gemning. Benchmarket tjekker først, at begge operationer returnerede 1 og rapporterede det godkendte sideantal, og validerer derefter den enkelte gemte PDF i denne rækkefølge:

Tre output-gates i PDFlibPas: begge operationer skal returnere 1 med det godkendte PageCount, en uafhængig checker skal bestå den gemte fil uden advarsler, hver side skal renderes til et per-side image SHA-256-sæt, der matcher kilden, og nonvisuel semantik skal matche på optional content og målestrukturer
En gemning, der hurtigt skriver en ødelagt PDF, er ikke en hurtigere gemning, så en timing tæller kun, når struktur, rendering og nonvisuel semantik alle er enige om, at outputtet stadig er det samme dokument
  • Struktur: en uafhængig PDF-checker skal bestå den gemte fil uden fejl eller advarsler
  • Rendering: hver side renderes i sin standardtilstand, og per-side image SHA-256-sættet skal matche referencerenderingen af den godkendte kilde præcist
  • Nonvisuel semantik: en separat semantisk sammenligning mod kilden dækker udvalgte egenskaber, som pixels ikke kan vise, herunder optional-content- og målestrukturer inden for deres dokumenterede omfang

Med de gates på plads kørte den fulde lokale corpus-matrix proben på FPC Win32, FPC Win64, Delphi Win32 og Delphi Win64 over 12 godkendte PDF'er med 1.612 kildesider, hvilket gav 48 sample/target-par og 6.448 validerede outputsider uden udvalgte semantiske forskelle. Alle 96 operationsmålinger bevarede positive rå counter-værdier, der stemte med deres rapporterede sekunder, og de værdier aggregeres bevidst ikke til en cross-compiler-hastighedstabel, fordi matrixen er funktionelt bevis snarere end en kontrolleret sammenligning. Load/save-stien påstår heller ikke at dekode alle embeddede billeder, validere signaturer, eksekvere XFA eller certificere PDF/UA; hvis du skal bedømme rendering-throughput frem for load/save-omkostninger, er concurrency-begrænsningerne i parallel side-rendering og thread safety i PDF Library for Delphi det bedre udgangspunkt

Det praktiske takeaway er kort: gem de rå counters, alternér rækkefølgen, gate starten, afvis noisy spreads, og tim aldrig et output, du ikke har valideret. De regler er, hvad der gør, at PDF Library for Delphi kan sige "ingen målbar ændring" lige så overbevisende som "hurtigere", og samme probe-kilde kompilerer uændret på Delphi og FPC for Win32 og Win64. Du kan gennemse biblioteket, dets load/save-API og de understøttede compilers på produktsiden for PDF Library for Delphi