Technischer Artikel

Ehrliche PDF-Load/Save-Benchmarks in Delphi: Noise Gates

Ein ehrlicher Load/Save-Benchmark für PDF Library for Delphi misst LoadFromFile und SaveToFile mit QueryPerformanceCounter, behält die rohen Ticks und die Counter-Frequenz, fährt Baseline und Kandidat in alternierenden A/B-, B/A-, A/B-Paaren, startet nicht, solange die CPU-Last über 25 % liegt, verwirft jedes Ergebnis, dessen Range-zu-Median-Streuung über 15 % liegt, und wirft jede Zeitmessung weg, deren gespeichertes PDF die strukturelle, Rendering- oder semantische Validierung nicht besteht. Diese Liste liest sich wie Bürokratie, bis zum ersten Mal, wenn eine Behauptung „20 % schneller“ beim Wiederholungslauf verdampft. Was folgt, ist der Weg, auf dem der dedizierte Korpus-Probe und sein Vergleichsläufer dorthin gekommen sind, inklusive des Laufs, in dem die Maschine schlicht zu beschäftigt war, um irgendetwas zu messen, und das Harness das korrekt gesagt hat

Warum meldet ein Delphi-PDF-Benchmark null Sekunden?

Ein PDF-Load-Benchmark meldet null Sekunden, wenn seine Uhr gröber tickt als die Operation, die sie misst, und GetTickCount64 ist genau so eine Uhr: Sie liefert Millisekunden zurück, aber auf Windows läuft sie nur weiter, wenn der System-Timer-Interrupt feuert, üblicherweise alle 15,6 ms. Der FPC-Port der Large-File-Benchmark-Demo in PDF Library for Delphi nutzte sie, weil es TStopwatch in dieser Toolchain nicht gibt, und zeichnet die verstrichene Zeit mit drei Nachkommastellen auf. Das Laden einer kleinen CAD-Zeichnung oder eines kurzen tagged Dokuments läuft weit innerhalb eines Timer-Schritts ab, also druckte die Demo manchmal 0.000 für ein Laden, das nachweislich echte Arbeit gemacht hat

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

// innerhalb der Operationsschleife
Lib:= TPDFlib.Create;
try
  Lib.OnProgress:= Reporter.Progress;
  Started:= GetTickCount64;
  LoadCode:= Lib.LoadFromFile(InputFile, Password);
  ...

Eine Null ist schlechter als eine unpräzise Zahl, denn jeder Vergleich, den Sie darauf bauen, dividiert durch sie. Der Paarvergleichsläufer behandelt jeden Arm mit einem Null-Minimum als inkonklusiv, mit der Begründung „Zero duration prevents a meaningful ratio“ – die korrekte Verweigerung, aber sie bedeutet auch, dass die Demo-Zeiten genau dort eine Messlücke ließen, wo kurze Dateien leben. Dieselbe Demo installiert außerdem einen OnProgress-Callback, ihre Zeiten enthalten also Callback-Overhead, den eine saubere Load/Save-Messung nicht tragen sollte, und archivierte Demo-Zahlen sind mit nichts austauschbar, was später gemessen wurde

LoadFromFile und SaveToFile mit QueryPerformanceCounter messen

Der dedizierte Konsolen-Probe, Tests/CorpusLoadSave.dpr, misst zwei Operationen pro Eingabedatei mit QueryPerformanceCounter: LoadFromFile plus Auslesen von PageCount, und LoadFromFile plus PageCount plus SaveToFile. Jede Operation bekommt eine frische TPDFlib-Instanz und keinen Progress-Callback, und Instanz-Konstruktor und -Destruktor liegen außerhalb der gemessenen Region, ebenso das CSV-Schreiben und die gesamte Ausgabevalidierung. Der Counter wird unmittelbar vor dem Laden und unmittelbar nach dem letzten Bibliotheksaufruf gelesen, und LastErrorCode wird erst nach der zweiten Messung abgeholt

Gemessene Region des PDFlibPas-Korpus-Probes: QueryPerformanceCounter wird unmittelbar vor LoadFromFile und erneut nach dem letzten Bibliotheksaufruf gelesen, mit PageCount und SaveToFile innerhalb, während Instanzaufbau, CSV-Schreiben, Ausgabevalidierung und das Abholen des Fehlercodes außerhalb der gemessenen Region bleiben
Roh-Ticks und Counter-Frequenz werden neben den abgeleiteten Sekunden notiert, sodass eine CAD-Zeichnung, die in 8.888 Ticks bei zehn Millionen Ticks pro Sekunde lädt, als echte Daten erhalten bleibt, statt auf null gerundet zu werden
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');

Der Probe schreibt die rohe Tick-Anzahl und die Counter-Frequenz neben den abgeleiteten Sekunden weg, formatiert mit neun Nachkommastellen und festem . als Dezimaltrennzeichen, sodass jeder den Quotienten aus dem CSV nachrechnen kann, statt ihm zu vertrauen. Auf dem FPC-Win64-Build lud die CAD-Stichprobe in 8.888 Ticks bei 10.000.000 Ticks pro Sekunde, notiert als 0.000888800 Sekunden – eine Beobachtung, die der alte Timer auf null gerundet hätte. Der Probe schneidet kurze Werte bewusst nicht ab, ersetzt sie nicht durch eine Mindestdauer und zieht keinen geschätzten Timer-Overhead ab, und er schreibt beide Zeilen trotzdem weg, mit Exit-Code ungleich null, wenn ein Bibliotheksaufruf fehlschlägt. Neun Stellen sind keine Genauigkeit: Mehr notierte Präzision sagt nichts über Wiederholbarkeit, und verrauschte oder nuller Beobachtungen müssen weiterhin downstream verworfen werden

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

Was macht einen PDF-Load/Save-Zeitvergleich vertrauenswürdig?

Ein Zeitvergleich zwischen zwei Builds der PDF Library for Delphi ist nur dann vertrauenswürdig, wenn Startreihenfolge, Startbedingungen und Streuung kontrolliert und notiert sind, also plant der Vergleichsläufer mindestens drei Paare in A/B-, B/A-, A/B-Reihenfolge. Immer zuerst die Baseline laufen zu lassen schiebt dem Kandidaten still einen wärmeren File-Cache und einen anderen thermischen Zustand zu; die Reihenfolge zu alternieren verteilt diesen Bias über beide Arme, statt ihn einem gutzuschreiben. Vor jedem Arm hasht der Läufer die komplette Eingabedatei mit SHA-256, was zum einen verifiziert, dass sich nichts geändert hat, und zum anderen dieselben Bytes für beide Arme vorliest, und er hasht die beiden Executables und die Validierungswerkzeuge nach jedem Lauf erneut, damit eine neu gebaute Binary nicht mitten in einer Serie einschleichen kann

Danach sampelt der Läufer die CPU-Auslastung maschinenweit einmal pro Sekunde und startet den Arm erst, wenn ein Sample auf 25 % oder darunter fällt, höchstens 30 Sekunden wartend, bevor er den Versuch als abgelehnt notiert. Diese Sperre kontrolliert die Startbedingung und sonst nichts: Sie isoliert die Maschine nicht während des Laufs, und Energiezustand, Thermal-Throttling, Hintergrundarbeit und OS-Caching können die Zahlen weiterhin bewegen. Der zweite Filter ist daher statistisch, im schlichtesten Sinne. Pro Operation berechnet der Läufer Range geteilt durch Median für den Baseline-Arm, den Kandidaten-Arm und die Verteilung der gepaarten Kandidat/Baseline-Verhältnisse, und überschreitet eine der drei Größen 0,15, wird das Ergebnis als verrauscht etikettiert, statt als Befund gemeldet

Paarvergleichs-Sperren in PDFlibPas: Drei Paare laufen in A/B-, B/A-, A/B-Reihenfolge mit SHA-256-Eingabe-Hashing vor jedem Arm, eine Startsperre wartet auf CPU von höchstens 25 Prozent, und Range-zu-Median-Streuungen über 0,15 auf LoadFromFile oder dem Load-plus-SaveToFile-Arm etikettieren den Lauf als verrauscht
Die alternierende Startreihenfolge verteilt Cache- und Thermik-Bias über beide Arme, und die Same-Binary-Kontrolle zeigt, was ein solcher Aufbau beweisen kann: Verhältnisse nahe 1,0 belegen Wiederholbarkeit, niemals eine Speedup-Behauptung

Warum beweist eine Same-Binary-Kontrolle Wiederholbarkeit, nicht Geschwindigkeit?

Eine Same-Binary-Kontrolle läuft mit identischen Executables als Baseline und Kandidat, also kann ein Verhältnis nahe 1,0 nur belegen, dass der Messaufbau sich selbst wiederholt; es kann niemals zeigen, dass eine Implementierung schneller geworden ist. Die erste strikte Kontrolle am 21.09.2026 nutzte den hochauflösenden FPC-Win64-Probe gegen eine zugelassene 70-seitige tagged Anleitung, und alle sechs Starts wurden abgelehnt, weil die CPU-Samples zwischen 26,5 % und 93,8 % lagen. Der Bericht enthielt Fehler und keine Aggregate – genau das Ergebnis, das Sie wollen, wenn die Maschine beschäftigt ist. Ein Retry am selben Tag mit byte-identischen Eingaben, demselben Probe-Executable und unveränderten Schwellen nahm alle sechs Starts innerhalb von 3 Sekunden an; jede Range-zu-Median-Streuung landete zwischen 0,019 und 0,054, und die Verhältnis-Mediane waren 1,0084 für LoadFromFile und 0,9872 für LoadFromFile + SaveToFile

Dieses Paar von Zahlen etabliert ein qualifiziertes Beobachtungsfenster und nichts weiter. Wenn sich die beiden Binaries unterscheiden, wird ein stabiler Lauf als deskriptiver Vergleich etikettiert, mit der ausdrücklichen Notiz, dass die Verhältnisse Beobachtungen sind, keine statistische Signifikanz und keine Speedup-Behauptung. Die Disziplin zählt am meisten, wenn Sie gezielte Optimierungen validieren wie die in Profiling der PDF Library for Delphi und Ersatz von Hot Paths durch Hash-Indizes beschriebenen: Ein Profiler sagt Ihnen, wohin die Zeit fließt, aber nur ein kontrollierter gepaarter Lauf über echte Dokumente sagt Ihnen, ob die Änderung den Kontakt mit der gesamten Pipeline überlebt hat. Eine Grenze noch, die man laut aussprechen sollte — normal-save enthält das Laden, und das Peak-Working-Set, das der Läufer notiert, ist prozessweit, also steckt darin kein Speicher, der allein aufs Speichern entfällt

Drei Ausgabe-Sperren und eine Vier-Compiler-Matrix

Keine Zeitmessung der PDF Library for Delphi zählt, solange nicht die von ihr erzeugte Datei drei unabhängige Sperren passiert, denn ein Save, der schnell ein kaputtes PDF schreibt, ist kein schnellerer Save. Der Benchmark prüft zuerst, dass beide Operationen 1 zurückgaben und die zugelassene Seitenzahl meldeten, und validiert dann das einzelne gespeicherte PDF in dieser Reihenfolge:

Drei Ausgabe-Sperren in PDFlibPas: Beide Operationen müssen 1 mit der zugelassenen PageCount zurückgeben, ein unabhängiger Checker muss die gespeicherte Datei ohne Warnungen bestehen lassen, jede Seite muss auf ein Per-Page-Image-SHA-256-Set gerendert werden, das zur Quelle passt, und die nichtvisuelle Semantik muss bei Optional Content und Messstrukturen übereinstimmen
Ein Save, der schnell ein kaputtes PDF schreibt, ist kein schnellerer Save, eine Zeitmessung zählt also erst, wenn Struktur, Rendering und nichtvisuelle Semantik übereinstimmen, dass die Ausgabe noch dasselbe Dokument ist
  • Struktur: Ein unabhängiger PDF-Checker muss die gespeicherte Datei ohne Fehler und Warnungen bestehen lassen
  • Rendering: Jede Seite wird in ihrem Standardzustand gerendert, und das Per-Page-Image-SHA-256-Set muss exakt dem Referenz-Rendering der zugelassenen Quelle entsprechen
  • Nichtvisuelle Semantik: Ein separater semantischer Vergleich gegen die Quelle deckt ausgewählte Eigenschaften ab, die Pixel nicht zeigen können, darunter Optional-Content- und Messstrukturen innerhalb ihres dokumentierten Umfangs

Mit diesen Sperren lief die komplette lokale Korpus-Matrix: der Probe auf FPC Win32, FPC Win64, Delphi Win32 und Delphi Win64 über 12 zugelassene PDFs mit 1.612 Quellseiten, also 48 Sample/Target-Paare und 6.448 validierte Ausgabeseiten ohne ausgewählte semantische Differenzen. Alle 96 Operationsmessungen behielten positive rohe Counter-Werte konsistent mit ihren gemeldeten Sekunden, und diese Werte werden bewusst nicht zu einer Cross-Compiler-Speed-Tabelle aggregiert, denn die Matrix ist funktionaler Beleg, kein kontrollierter Vergleich. Der Load/Save-Pfad behauptet auch nicht, jedes eingebettete Bild zu dekodieren, Signaturen zu validieren, XFA auszuführen oder PDF/UA zu zertifizieren; wenn Sie den Rendering-Durchsatz beurteilen wollen statt der Load/Save-Kosten, sind die Nebenläufigkeits-Constraints in Parallel-Page-Rendering und Thread-Safety in PDF Library for Delphi der bessere Startpunkt

Die praktische Takeaway ist kurz: Roh-Counter behalten, die Reihenfolge alternieren, den Start absperren, verrauschte Streuungen verweigern und niemals eine Ausgabe messen, die Sie nicht validiert haben. Diese Regeln ermöglichen es der PDF Library for Delphi, „keine messbare Änderung“ genauso selbstbewusst zu sagen wie „schneller“, und dieselbe Probe-Quelle kompiliert unverändert auf Delphi und FPC für Win32 und Win64. Die Bibliothek, ihre Load/Save-API und die unterstützten Compiler können Sie auf der Produktseite der PDF Library for Delphi ansehen