Teknisk artikel

Ärliga PDF load/save-benchmarks i Delphi: brusgränser

En ärlig load/save-benchmark för PDF Library for Delphi mäter LoadFromFile och SaveToFile med QueryPerformanceCounter, sparar råa tick och counterfrekvens, kör baseline och kandidat i alternerande A/B-, B/A-, A/B-par, vägrar starta medan CPU-lasten ligger över 25 %, avvisar varje resultat vars range/median-spridning överstiger 15 % och kastar varje mätning vars sparade PDF inte klarar strukturell, renderings- eller semantisk validering. Listan låter som byråkrati ända till första gången ett påstående om "20 % snabbare" dunstar vid en omkörning. Det som följer är hur den dedikerade corpus-sonden och dess jämförelserunner kom dit, inklusive körningen då maskinen helt enkelt var för upptagen för att mäta något och testriggen korrekt sa det

Varför rapporterar en Delphi-PDF-benchmark noll sekunder?

En PDF-inläsningsbenchmark rapporterar noll sekunder när dess klocka tickar grövre än den operation den mäter, och GetTickCount64 är precis den typen av klocka: den returnerar millisekunder, men på Windows går den bara framåt när systemtimerns avbrott utlöses, vanligen var 15,6 ms. FPC-porten av benchmarkdemon för enorma filer i PDF Library for Delphi använde den eftersom TStopwatch inte finns i den verktygskedjan, och den registrerar förfluten tid med tre decimaler. Att läsa in en liten CAD-ritning eller ett kort taggat dokument tar slut väl inom ett timersteg, så demon skrev ibland ut 0.000 för en inläsning som uppenbarligen gjorde riktigt arbete

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

// inne i operationsloopen
Lib:= TPDFlib.Create;
try
  Lib.OnProgress:= Reporter.Progress;
  Started:= GetTickCount64;
  LoadCode:= Lib.LoadFromFile(InputFile, Password);
  ...

En nolla är värre än ett oprecist tal, för att varje jämförelse du bygger på den dividerar med den. Den parade jämförelserunnern behandlar varje arm med noll som minimum som inkonklusiv med motiveringen "Zero duration prevents a meaningful ratio", vilket är den korrekta vägran, men det betyder också att demotiderna lämnade ett mäthål exakt där korta filer finns. Samma demo installerar dessutom en OnProgress-callback, så dess tider inkluderar callback-overhead som en ren load/save-mätning inte borde bära, och arkiverade demosiffror går inte att byta mot något som mäts senare

Mäta LoadFromFile och SaveToFile med QueryPerformanceCounter

Den dedikerade konsolsonden, Tests/CorpusLoadSave.dpr, mäter två operationer per infil med QueryPerformanceCounter: LoadFromFile plus läsning av PageCount, och LoadFromFile plus PageCount plus SaveToFile. Varje operation får en färsk TPDFlib-instans och ingen progress-callback, och instansens konstruktor och destruktor ligger utanför det tidmätta området, liksom CSV-skrivning och all utdatavalidering. Countern läses direkt före inläsningen och direkt efter det sista biblioteksanropet, och LastErrorCode hämtas först efter den andra avläsningen

Tidmätt region i PDFlibPas corpus-sond: QueryPerformanceCounter läses direkt före LoadFromFile och igen efter det sista biblioteksanropet, med PageCount och SaveToFile innanför, medan instansuppsättning, CSV-skrivning, utdatavalidering och hämtning av felkoden alla ligger utanför det tidmätta området
Råa tick och counterfrekvens registreras bredvid de härledda sekunderna, så en CAD-ritning som läses in på 8 888 tick vid tio miljoner tick per sekund bevaras som verklig data i stället för att avrundas till noll
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');

Sonden skriver det råa tickantalet och counterfrekvensen bredvid de härledda sekunderna, formaterade med nio decimaler och en fast . som decimaltecken, så vem som helst kan räkna ut kvoten från CSV-filen i stället för att lita på den. På FPC Win64-bygget lästes CAD-exemplet in på 8 888 tick vid 10 000 000 tick per sekund, registrerat som 0.000888800 sekunder — en observation som den gamla timern skulle ha avrundat till noll. Sonden klipper medvetet inte korta värden, ersätter inte med en minimilängd och drar inte av en uppskattad timeroverhead, och den skriver fortfarande båda raderna med en icke-noll exitkod när ett biblioteksanrop misslyckas. Nio siffror är dock inte noggrannhet: mer registrerad precision säger inget om repeterbarhet, och brusiga eller nollobservationer måste fortfarande avvisas längre ned i kedjan

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

Vad gör en jämförelse av PDF load/save-tider pålitlig?

En tidsjämförelse mellan två byggen av PDF Library for Delphi är bara pålitlig när startordning, startvillkor och spridning alla styrs och registreras, så jämförelserunnern schemalägger minst tre par i ordningen A/B, B/A, A/B. Att alltid köra baseline först ger i smyg kandidaten ett varmare filcache och ett annat termiskt tillstånd; att alternera ordningen sprider den biasen över båda armarna i stället för att kreditera den till en. Före varje arm hashar runnern hela infilen med SHA-256, vilket både verifierar att ingenting ändrats och försläser samma byte för vilken arm som helst, och den hashar de två körbara filerna och valideringsverktygen igen efter varje körning så att en ombyggd binär inte kan smyga in mitt i en serie

Sedan samplar runnern CPU-utnyttjandet för hela maskinen en gång per sekund och startar armen först när ett sample faller till 25 % eller under, med väntan i högst 30 sekunder innan försöket registreras som avvisat. Den grinden styr startvillkoret och inget annat: den isolerar inte maskinen under körningen, och strömläge, termisk nedskalning, bakgrundsarbete och OS-cache kan fortfarande flytta talen. Det andra filtret är därför statistiskt, helt enkelt. För varje operation beräknar runnern range delat med median för baseline-armen, kandidatarmen och fördelningen av parade kandidat/baseline-kvoter, och om någon av de tre överstiger 0,15 etiketteras resultatet brusigt i stället för att rapporteras som ett fynd

Parade jämförelsegrindar i PDFlibPas: tre par körs i ordningen A/B, B/A, A/B med SHA-256-hashning av infilen före varje arm, en startgrind väntar på CPU på eller under 25 procent, och range/median-spridningar över 0,15 på LoadFromFile eller armen load plus SaveToFile etiketterar körningen som brusig
Att alternera startordningen sprider cache- och termisk bias över båda armarna, och same-binary-kontrollen visar vad ett sådant upplägg kan bevisa: kvoter nära 1,0 etablerar repeterbarhet, aldrig ett påstående om snabbare

Varför bevisar en same-binary-kontroll repeterbarhet, inte fart?

En same-binary-kontroll kör identiska körbara filer som baseline och kandidat, så en kvot nära 1,0 kan bara bevisa att mätuppsättningen upprepar sig själv; den kan aldrig visa att en implementation blivit snabbare. Den första strikta kontrollen den 2026-09-21 använde högupplösta FPC Win64-sonden mot en antagen 70-sidig taggad guide, och alla sex starter avvisades eftersom CPU-samplen spände från 26,5 % till 93,8 %. Rapporten innehöll misslyckanden och inga aggregerade värden, vilket är exakt det utfall du vill ha när maskinen är upptagen. Ett omförsök samma dag med byte-identiska ingångar, samma sondkörbara fil och oförändrade trösklar accepterade alla sex starter inom 3 sekunder; varje range/median-spridning hamnade mellan 0,019 och 0,054, och kvotmedianerna var 1,0084 för LoadFromFile och 0,9872 för LoadFromFile + SaveToFile

Det paret tal etablerar ett kvalificerat observationsfönster och ingenting mer. När de två binärfilerna skiljer sig etiketteras en stabil körning som en deskriptiv jämförelse, med den uttryckliga noteringen att kvoterna är observationer, inte statistisk signifikans eller ett påstående om snabbare. Disiplinen spelar störst roll när du validerar riktade optimeringar som de som beskrivs i profilering av PDF Library for Delphi och att ersätta heta vägar med hashindex: en profilerare talar om var tiden går, men bara en kontrollerad parad körning på riktiga dokument talar om huruvida ändringen överlevde kontakten med hela pipelinen. En gräns till värd att säga högt — normal-save inkluderar inläsning, och den peak working set som runnern registrerar är processövergripande, så ingenting av den är minne som kan tillskrivas enbart sparandet

Tre utdatagrindar och en matris med fyra kompilatorer

Ingen mätning av PDF Library for Delphi räknas om inte filen den producerade passerar tre oberoende grindar, för ett sparande som snabbt skriver en sönder PDF är inte ett snabbare sparande. Benchmarket kontrollerar först att båda operationerna returnerade 1 och rapporterade det antagna sidantalet, och validerar sedan den enda sparade PDF:en i denna ordning:

Tre utdatagrindar i PDFlibPas: båda operationerna måste returnera 1 med det antagna PageCount, en oberoende kontrollant måste godkänna den sparade filen utan varningar, varje sida måste renderas till en uppsättning per-sidiga bild-SHA-256 som matchar källan, och icke-visuell semantik måste matcha på optional content och mätstrukturer
Ett sparande som snabbt skriver en sönder PDF är inte ett snabbare sparande, så en mätning räknas bara när struktur, rendering och icke-visuell semantik alla instämmer i att utdatan fortfarande är samma dokument
  • Struktur: en oberoende PDF-kontrollant måste godkänna den sparade filen utan fel eller varningar
  • Rendering: varje sida renderas i sitt standardläge, och uppsättningen per-sidiga bild-SHA-256 måste exakt matcha referensrenderingen av den antagna källan
  • Icke-visuell semantik: en separat semantisk jämförelse mot källan täcker utvalda egenskaper som pixlar inte kan visa, inklusive optional content- och mätstrukturer inom deras dokumenterade omfattning

Med de grindarna på plats körde den lokala corpusmatrisen sonden på FPC Win32, FPC Win64, Delphi Win32 och Delphi Win64 över 12 antagna PDF:er med 1 612 källsidor, vilket gav 48 sample/target-par och 6 448 validerade utdatasidor utan utvalda semantiska skillnader. Alla 96 operationsmätningar behöll positiva råa countervärden som stämmer med sina rapporterade sekunder, och de värdena aggregeras medvetet inte till en hastighetstabell över kompilatorer, eftersom matrisen är funktionell evidens snarare än en kontrollerad jämförelse. Load/save-vägen hävdar heller inte att den avkodar varje inbäddad bild, validerar signaturer, kör XFA eller certifierar PDF/UA; om du behöver bedöma renderingsgenomströmning snarare än load/save-kostnad är samtidighetskraven i parallell sidrendering och trådsäkerhet i PDF Library for Delphi den bättre startpunkten

Den praktiska lärdomen är kort: spara råa counters, alternera ordningen, grinda starten, vägra brusiga spridningar och mät aldrig en utdata du inte har validerat. De reglerna är det som låter PDF Library for Delphi säga "ingen mätbar förändring" lika övertygande som "snabbare", och samma sondkällkod kompileras oförändrad på Delphi och FPC för Win32 och Win64. Du kan granska biblioteket, dess load/save-API och de kompilatorer som stöds på produktsidan för PDF Library for Delphi