Un benchmark load/save honnête pour PDF Library for Delphi chronomètre LoadFromFile et SaveToFile avec QueryPerformanceCounter, garde les ticks bruts et la fréquence du compteur, fait tourner baseline et candidate en paires alternées A/B, B/A, A/B, refuse de démarrer tant que la charge CPU dépasse 25 %, rejette tout résultat dont le rapport intervalle/médiane dépasse 15 %, et jette chaque mesure dont le PDF sauvegardé échoue à la validation structurelle, de rendu ou sémantique. La liste a des allures de bureaucratie jusqu'à la première fois qu'une revendication « 20 % plus rapide » s'évapore à la relance. Ce qui suit, c'est comment la sonde dédiée de corpus et son runner de comparaison en sont arrivés là, y compris la session où la machine était simplement trop occupée pour mesurer quoi que ce soit et où le harnais l'a correctement dit
Pourquoi un benchmark PDF Delphi rapporte-t-il zéro seconde ?
Un benchmark de chargement PDF rapporte zéro seconde quand son horloge ticke plus grossièrement que l'opération mesurée, et GetTickCount64 est exactement ce genre d'horloge : elle renvoie des millisecondes, mais sous Windows elle n'avance que quand l'interruption du timer système se déclenche, en général toutes les 15,6 ms. Le portage FPC de la démo de benchmark de gros fichiers de PDF Library for Delphi l'utilisait parce que TStopwatch n'est pas disponible dans cette toolchain, et il enregistre le temps écoulé avec trois décimales. Charger un petit dessin CAO ou un court document taggé se termine bien à l'intérieur d'un pas du timer, donc la démo affichait parfois 0.000 pour un chargement qui faisait clairement du vrai travail
function ElapsedSeconds(StartTick: QWord): Double;
begin
Result:= (GetTickCount64- StartTick)/ 1000.0;
end;
// dans la boucle d'opération
Lib:= TPDFlib.Create;
try
Lib.OnProgress:= Reporter.Progress;
Started:= GetTickCount64;
LoadCode:= Lib.LoadFromFile(InputFile, Password);
...
Un zéro est pire qu'un nombre imprécis, parce que toute comparaison que vous bâtissez dessus divise par lui. Le runner de comparaison appariée traite toute branche dont le minimum vaut zéro comme non concluante, avec le motif « Zero duration prevents a meaningful ratio », refus correct, mais cela signifie aussi que les mesures de la démo laissaient un trou de mesure exactement là où vivent les fichiers courts. La même démo installe aussi un callback OnProgress, donc ses mesures incluent le surcoût du callback qu'une mesure propre de load/save ne devrait pas porter, et les chiffres archivés de la démo ne sont pas interchangeables avec quoi que ce soit de mesuré plus tard
Chronométrer LoadFromFile et SaveToFile avec QueryPerformanceCounter
La sonde console dédiée, Tests/CorpusLoadSave.dpr, mesure deux opérations par fichier d'entrée avec QueryPerformanceCounter : LoadFromFile plus la lecture de PageCount, et LoadFromFile plus PageCount plus SaveToFile. Chaque opération reçoit une instance TPDFlib toute neuve et pas de callback de progression, et le constructeur et le destructeur de l'instance restent hors de la zone chronométrée, tout comme l'écriture CSV et toute la validation de sortie. Le compteur est lu immédiatement avant le chargement et immédiatement après le dernier appel à la bibliothèque, et LastErrorCode n'est consulté qu'après la seconde lecture
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 sonde écrit le nombre de ticks brut et la fréquence du compteur à côté des secondes dérivées, formatées avec neuf décimales et un séparateur décimal . fixe, pour que chacun puisse recalculer le quotient depuis le CSV au lieu de lui faire confiance. Sur la build FPC Win64, l'échantillon CAO s'est chargé en 8 888 ticks à 10 000 000 de ticks par seconde, enregistré 0.000888800 seconde — une observation que l'ancien timer aurait arrondie à zéro. La sonde ne rogne délibérément pas les valeurs courtes, ne substitue pas de durée minimale et ne soustrait pas un surcoût de timer estimé, et elle écrit toujours les deux lignes avec un code de sortie non nul quand un appel à la bibliothèque échoue. Neuf chiffres ne sont pas de la précision, pourtant : plus de décimales enregistrées ne dit rien de la répétabilité, et les observations bruitées ou nulles doivent toujours être rejetées en aval
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));
Qu'est-ce qui rend une comparaison de mesures load/save PDF fiable ?
Une comparaison de mesures entre deux builds de PDF Library for Delphi n'est fiable que si l'ordre de démarrage, les conditions de départ et la dispersion sont tous contrôlés et enregistrés, alors le runner de comparaison planifie au moins trois paires dans l'ordre A/B, B/A, A/B. Faire toujours tourner la baseline en premier offre discrètement à la candidate un cache fichier plus chaud et un état thermique différent ; alterner l'ordre répartit ce biais sur les deux branches au lieu de le porter au crédit de l'une. Avant chaque branche, le runner hache le fichier d'entrée complet en SHA-256, ce qui vérifie à la fois que rien n'a changé et pré-lit les mêmes octets pour l'une ou l'autre branche, et il re-hache les deux exécutables et les outils de validation après chaque session pour qu'un binaire reconstruit ne puisse pas se glisser au milieu d'une série
Le runner échantillonne ensuite l'utilisation CPU de toute la machine une fois par seconde et ne démarre la branche que quand un échantillon descend à 25 % ou moins, en attendant 30 secondes au plus avant d'enregistrer la tentative comme rejetée. Ce garde-fou contrôle la condition de départ et rien d'autre : il n'isole pas la machine pendant la session, et l'état d'alimentation, le throttling thermique, le travail en fond et le cache de l'OS peuvent toujours bouger les chiffres. Le second filtre est donc statistique au sens le plus plat. Pour chaque opération, le runner calcule l'intervalle divisé par la médiane pour la branche baseline, la branche candidate et la distribution des ratios appariés candidate/baseline, et si l'un des trois dépasse 0,15 le résultat est étiqueté bruité au lieu d'être rapporté comme un constat
Pourquoi un contrôle même-binaire prouve la répétabilité, pas la vitesse ?
Un contrôle même-binaire fait tourner des exécutables identiques comme baseline et candidate, donc un ratio proche de 1,0 ne peut que prouver que le dispositif de mesure se répète lui-même ; il ne peut jamais montrer qu'une implémentation est devenue plus rapide. Le premier contrôle strict, le 2026-09-21, utilisait la sonde FPC Win64 haute résolution contre un guide taggé admis de 70 pages, et les six démarrages ont tous été rejetés parce que les échantillons CPU allaient de 26,5 % à 93,8 %. Le rapport contenait des échecs et aucun agrégat, ce qui est exactement le résultat voulu quand la machine est occupée. Une relance le jour même, avec des entrées identiques à l'octet près, le même exécutable de sonde et des seuils inchangés, a accepté les six démarrages en moins de 3 secondes ; chaque rapport intervalle/médiane est tombé entre 0,019 et 0,054, et les médianes des ratios étaient de 1,0084 pour LoadFromFile et 0,9872 pour LoadFromFile + SaveToFile
Cette paire de nombres établit une fenêtre d'observation qualifiée et rien de plus. Quand les deux binaires diffèrent, une session stable est étiquetée comparaison descriptive, avec la note explicite que les ratios sont des observations, pas une significativité statistique ni une revendication de vitesse. La discipline compte le plus quand vous validez des optimisations ciblées comme celles décrites dans le profiling de PDF Library for Delphi et le remplacement des chemins chauds par des index de hachage : un profiler dit où va le temps, mais seule une session appariée contrôlée sur de vrais documents dit si la modification a survécu au contact de tout le pipeline. Une dernière frontière à dire à voix haute — le normal-save inclut le chargement, et le pic de working set enregistré par le runner est à l'échelle du processus, donc rien de tout cela n'est de la mémoire attribuable à la seule sauvegarde
Trois garde-fous de sortie et une matrice à quatre compilateurs
Aucune mesure de PDF Library for Delphi ne compte tant que le fichier produit ne passe pas trois garde-fous indépendants, car une sauvegarde qui écrit vite un PDF cassé n'est pas une sauvegarde plus rapide. Le benchmark vérifie d'abord que les deux opérations ont renvoyé 1 et rapporté le nombre de pages admis, puis valide l'unique PDF sauvegardé dans cet ordre :
- Structure : un checker PDF indépendant doit valider le fichier sauvegardé sans erreur ni avertissement
- Rendu : chaque page est rendue dans son état par défaut, et l'ensemble des SHA-256 d'image par page doit correspondre exactement au rendu de référence de la source admise
- Sémantique non visuelle : une comparaison sémantique séparée contre la source couvre des propriétés choisies que les pixels ne peuvent pas montrer, dont les structures optional-content et de mesure dans leur périmètre documenté
Garde-fous en place, la matrice complète du corpus local a fait tourner la sonde sur FPC Win32, FPC Win64, Delphi Win32 et Delphi Win64 sur 12 PDF admis totalisant 1 612 pages sources, soit 48 paires échantillon/cible et 6 448 pages de sortie validées sans différence sémantique sélectionnée. Les 96 mesures d'opération conservent toutes des valeurs de compteur brutes positives cohérentes avec leurs secondes rapportées, et ces valeurs ne sont délibérément pas agrégées en un tableau de vitesse inter-compilateurs, parce que la matrice est une preuve fonctionnelle plutôt qu'une comparaison contrôlée. Le chemin load/save ne prétend pas non plus décoder chaque image embarquée, valider des signatures, exécuter XFA ou certifier PDF/UA ; si vous devez juger du débit de rendu plutôt que du coût load/save, les contraintes de concurrence du rendu parallèle de pages et de la thread safety dans PDF Library for Delphi sont le meilleur point de départ
La leçon pratique tient en peu : gardez les compteurs bruts, alternez l'ordre, bloquez le départ, refusez les dispersions bruitées, et ne chronométrez jamais une sortie que vous n'avez pas validée. Ces règles sont ce qui permet à PDF Library for Delphi de dire « aucun changement mesurable » avec autant d'assurance que « plus rapide », et la même source de sonde compile à l'identique sur Delphi et FPC pour Win32 et Win64. Vous pouvez examiner la bibliothèque, son API load/save et les compilateurs pris en charge sur la page produit de PDF Library for Delphi