Articolo tecnico

Benchmark onesti load/save PDF in Delphi: gate anti rumore

Un benchmark load/save onesto per PDF Library for Delphi cronometra LoadFromFile e SaveToFile con QueryPerformanceCounter, conserva i tick grezzi e la frequenza del contatore, esegue baseline e candidate in coppie alternate A/B, B/A, A/B, rifiuta di partire mentre il carico CPU sta sopra il 25%, scarta ogni risultato il cui spread range/mediano supera il 15%, e butta via ogni timing il cui PDF salvato fallisce la validazione strutturale, di rendering o semantica. La lista sembra burocrazia finché la prima volta che una pretesa «20% più veloce» evapora al re-run. Quello che segue è come la probe dedicata sul corpus e il suo comparison runner sono arrivati fin lì, incluso il run in cui la macchina era semplicemente troppo occupata per misurare qualcosa e l'harness lo ha detto correttamente

Perché un benchmark PDF in Delphi riporta zero secondi?

Un benchmark di load PDF riporta zero secondi quando il suo orologio ticka più grossolanamente dell'operazione che misura, e GetTickCount64 è esattamente quel tipo di orologio: restituisce millisecondi, ma su Windows avanza solo quando scatta l'interrupt del timer di sistema, in genere ogni 15,6 ms. Il port FPC della demo di benchmark sui file enormi in PDF Library for Delphi lo usava perché TStopwatch non è disponibile in quella toolchain, e registrava il tempo trascorso con tre decimali. Caricare un disegno CAD piccolo o un documento tagged corto finisce ben dentro un passo del timer, quindi la demo a volte stampava 0.000 per un load che evidentemente faceva lavoro vero

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

// dentro il loop dell'operazione
Lib:= TPDFlib.Create;
try
  Lib.OnProgress:= Reporter.Progress;
  Started:= GetTickCount64;
  LoadCode:= Lib.LoadFromFile(InputFile, Password);
  ...

Uno zero è peggio di un numero impreciso, perché ogni confronto che ci costruisci sopra lo divide per lui. Il comparison runner a coppie tratta qualunque braccio con un minimo zero come inconcludente con la motivazione «Zero duration prevents a meaningful ratio», che è il rifiuto corretto, ma significa anche che i timing della demo lasciavano un buco di misura esattamente dove vivono i file corti. La stessa demo installa anche una callback OnProgress, quindi i suoi timing includono l'overhead della callback che una misura pulita di load/save non dovrebbe portarsi dietro, e i numeri archiviati della demo non sono interscambiabili con nulla di misurato dopo

Cronometare LoadFromFile e SaveToFile con QueryPerformanceCounter

La probe console dedicata, Tests/CorpusLoadSave.dpr, misura due operazioni per file di input con QueryPerformanceCounter: LoadFromFile più la lettura di PageCount, e LoadFromFile più PageCount più SaveToFile. Ogni operazione riceve un'istanza TPDFlib fresca e nessuna callback di progresso, e il costruttore e il distruttore dell'istanza stanno fuori dalla regione cronometrata, così come la scrittura CSV e tutta la validazione dell'output. Il contatore viene letto subito prima del load e subito dopo l'ultima chiamata alla libreria, e LastErrorCode viene letto solo dopo la seconda lettura

Regione cronometrata della probe sul corpus di PDFlibPas: QueryPerformanceCounter viene letto subito prima di LoadFromFile e di nuovo dopo l'ultima chiamata alla libreria, con PageCount e SaveToFile dentro, mentre setup dell'istanza, scrittura CSV, validazione dell'output e lettura del codice errore restano tutti fuori
I tick grezzi e la frequenza del contatore vengono registrati accanto ai secondi derivati, così un disegno CAD che carica in 8.888 tick a dieci milioni di tick al secondo resta dato vero invece di venire arrotondato a zero
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');

La probe scrive il conteggio tick grezzo e la frequenza del contatore accanto ai secondi derivati, formattati con nove decimali e un separatore decimale fisso ., così chiunque può ricalcolare il quoziente dal CSV invece di fidarsi. Sulla build FPC Win64 il campione CAD ha caricato in 8.888 tick a 10.000.000 di tick al secondo, registrato come 0.000888800 secondi — un'osservazione che il vecchio timer avrebbe arrotondato a zero. La probe deliberatamente non taglia i valori corti, non sostituisce una durata minima e non sottrae un overhead stimato del timer, e quando una chiamata alla libreria fallisce scrive comunque entrambe le righe con exit code non nullo. Nove cifre non sono accuratezza però: più precisione registrata non dice nulla sulla ripetibilità, e le osservazioni rumorose o nulle vanno comunque scartate a valle

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

Cosa rende affidabile un confronto di timing load/save PDF?

Un confronto di timing tra due build di PDF Library for Delphi è affidabile solo quando ordine di partenza, condizioni di partenza e spread sono tutti controllati e registrati, quindi il comparison runner programma almeno tre coppie in ordine A/B, B/A, A/B. Far girare sempre prima la baseline consegna in silenzio alla candidate un file cache più caldo e uno stato termico diverso; alternare l'ordine sparge quel bias su entrambi i bracci invece di accreditarlo a uno. Prima di ogni braccio il runner fa l'hash del file di input completo con SHA-256, che sia verifica che nulla sia cambiato sia prelegge gli stessi byte per entrambi i bracci, e rifà l'hash dei due eseguibili e degli strumenti di validazione dopo ogni run, così un binario ricompilato non può sgusciare in mezzo a una serie

Il runner campiona poi l'utilizzo CPU di tutta la macchina una volta al secondo e fa partire il braccio solo quando un campione scende al 25% o sotto, aspettando al massimo 30 secondi prima di registrare il tentativo come rifiutato. Quel gate controlla la condizione di partenza e nient'altro: non isola la macchina durante il run, e stato di alimentazione, thermal throttling, lavoro in background e caching dell'OS possono comunque muovere i numeri. Quindi il secondo filtro è statistico nel senso più piatto. Per ogni operazione il runner calcola range diviso mediano per il braccio baseline, per il braccio candidate e per la distribuzione dei rapporti a coppie candidate/baseline, e se uno qualsiasi dei tre supera 0.15 il risultato viene etichettato noisy invece di essere riportato come finding

Gate del confronto a coppie in PDFlibPas: tre coppie girano in ordine A/B, B/A, A/B con hash SHA-256 dell'input prima di ogni braccio, un gate di partenza aspetta CPU al 25 percento o sotto, e spread range/mediano oltre 0.15 su LoadFromFile o sul braccio load più SaveToFile etichettano il run come rumoroso
Alternare l'ordine di partenza sparge il bias di cache e temperatura su entrambi i bracci, e il controllo same-binary mostra che cosa un setup simile può provare: rapporti vicini a 1.0 stabiliscono ripetibilità, mai una pretesa di speedup

Perché un controllo same-binary prova ripetibilità, non velocità?

Un controllo same-binary fa girare eseguibili identici come baseline e candidate, quindi un rapporto vicino a 1.0 può solo provare che il setup di misura si ripete; non può mai mostrare che un'implementazione è diventata più veloce. Il primo controllo severo del 2026-09-21 usava la probe FPC Win64 ad alta risoluzione contro una guida tagged di 70 pagine ammessa, e tutti e sei i start furono rifiutati perché i campioni CPU oscillavano dal 26,5% al 93,8%. Il report conteneva fallimenti e nessun aggregato, che è esattamente l'esito che vuoi quando la macchina è occupata. Un retry nello stesso giorno, con input byte-identici, lo stesso eseguibile della probe e soglie invariate, ha accettato tutti e sei i start entro 3 secondi; ogni spread range/mediano è finito tra 0.019 e 0.054, e i mediani dei rapporti sono stati 1.0084 per LoadFromFile e 0.9872 per LoadFromFile + SaveToFile

Quella coppia di numeri stabilisce una finestra di osservazione qualificata e nient'altro. Quando i due binari differiscono, un run stabile viene etichettato confronto descrittivo, con la nota esplicita che i rapporti sono osservazioni, non significatività statistica né una pretesa di speedup. La disciplina conta di più quando stai validando ottimizzazioni mirate come quelle descritte in profiling di PDF Library for Delphi e sostituzione dei percorsi caldi con hash index: un profiler ti dice dove va il tempo, ma solo un run a coppie controllato su documenti veri ti dice se la modifica è sopravvissuta al contatto con l'intera pipeline. Un confine ancora da dire ad alta voce — normal-save include il load, e il picco di working set che il runner registra è dell'intero processo, quindi niente di tutto ciò è memoria attribuibile al solo save

Tre gate di output e una matrice a quattro compilatori

Nessun timing di PDF Library for Delphi conta se non dopo che il file prodotto ha passato tre gate indipendenti, perché un save che scrive in fretta un PDF rotto non è un save più veloce. Il benchmark prima verifica che entrambe le operazioni abbiano restituito 1 e riportato il numero di pagine ammesso, poi valida l'unico PDF salvato in quest'ordine:

Tre gate di output in PDFlibPas: entrambe le operazioni devono restituire 1 con il PageCount ammesso, un checker indipendente deve passare il file salvato senza warning, ogni pagina deve renderizzarsi con un insieme di SHA-256 delle immagini per pagina che combaci con la sorgente, e la semantica non visiva deve combaciare su optional-content e strutture di measurement
Un save che scrive in fretta un PDF rotto non è un save più veloce, quindi un timing conta solo quando struttura, rendering e semantica non visiva concordano sul fatto che l'output è ancora lo stesso documento
  • Struttura: un checker PDF indipendente deve passare il file salvato senza errori né warning
  • Rendering: ogni pagina viene renderizzata nel suo stato predefinito, e l'insieme degli SHA-256 delle immagini per pagina deve combaciare esattamente con il rendering di riferimento della sorgente ammessa
  • Semantica non visiva: un confronto semantico separato contro la sorgente copre proprietà selezionate che i pixel non possono mostrare, incluse le strutture optional-content e di measurement entro il loro scopo documentato

Con quei gate in piedi, la matrice completa del corpus locale ha fatto girare la probe su FPC Win32, FPC Win64, Delphi Win32 e Delphi Win64 su 12 PDF ammessi con 1.612 pagine sorgente, per 48 coppie campione/target e 6.448 pagine di output validate senza differenze semantiche selezionate. Tutte le 96 misure delle operazioni hanno conservato valori grezzi del contatore positivi e coerenti con i secondi riportati, e quei valori deliberatamente non vengono aggregati in una tabella di velocità cross-compilatore, perché la matrice è evidenza funzionale, non un confronto controllato. Il percorso load/save inoltre non pretende di decodificare ogni immagine embedded, validare firme, eseguire XFA o certificare PDF/UA; se devi giudicare il throughput di rendering più che il costo di load/save, i vincoli di concorrenza in rendering parallelo delle pagine e thread safety in PDF Library for Delphi sono il punto di partenza migliore

Il takeaway pratico è corto: conserva i contatori grezzi, alterna l'ordine, metti un gate sulla partenza, rifiuta gli spread rumorosi, e non cronometrare mai un output che non hai validato. Sono quelle regole che permettono a PDF Library for Delphi di dire «nessuna variazione misurabile» con la stessa sicurezza di «più veloce», e la stessa sorgente della probe compila invariata su Delphi e FPC per Win32 e Win64. Puoi rivedere la libreria, la sua API load/save e i compilatori supportati sulla pagina di prodotto di PDF Library for Delphi