Τεχνικό Άρθρο

Ειλικρινή benchmarks load/save PDF στο Delphi: noise gates

Ένα ειλικρινές benchmark load/save για το PDF Library for Delphi μετρά τα LoadFromFile και SaveToFile με QueryPerformanceCounter, κρατά τα raw ticks και τη συχνότητα του counter, τρέχει baseline και candidate σε εναλλασσόμενα ζευγάρια A/B, B/A, A/B, αρνείται να ξεκινήσει όσο το CPU load κάθεται πάνω από 25%, απορρίπτει κάθε αποτέλεσμα του οποίου το range-to-median spread ξεπερνά το 15%, και πετάει κάθε timing του οποίου το αποθηκευμένο PDF αποτυγχάνει σε δομικό, rendering ή semantic έλεγχο. Η λίστα ακούγεται σαν γραφειοκρατία μέχρι την πρώτη φορά που μια ισχύς «20% γρηγορότερα» εξαερώνεται σε επανάληψη. Αυτό που ακολουθεί είναι πώς η αφιερωμένη corpus probe και ο comparison runner της έφτασαν εκεί, συμπεριλαμβανομένου του run όπου το μηχάνημα ήταν απλώς πολύ απασχολημένο για να μετρήσει οτιδήποτε και το harness το είπε σωστά

Γιατί ένα benchmark PDF στο Delphi αναφέρει μηδέν δευτερόλεπτα;

Ένα benchmark φόρτωσης PDF αναφέρει μηδέν δευτερόλεπτα όταν το ρολόι του χτυπά πιο χοντρικά από τη λειτουργία που μετρά, και το GetTickCount64 είναι ακριβώς τέτοιο ρολόι: επιστρέφει milliseconds, αλλά στα Windows προχωρά μόνο όταν πυροδοτείται το interrupt του system timer, συνήθως κάθε 15.6 ms. Το FPC port του demo benchmark huge-file στο PDF Library for Delphi το χρησιμοποιούσε επειδή το TStopwatch δεν είναι διαθέσιμο σε εκείνο το toolchain, και καταγράφει τον χρόνο με τρία δεκαδικά ψηφία. Η φόρτωση ενός μικρού CAD σχεδίου ή ενός σύντομου tagged εγγράφου τελειώνει καλά μέσα σε ένα βήμα timer, οπότε το demo μερικές φορές τύπωνε 0.000 για μια φόρτωση που προφανώς έκανε πραγματική δουλειά

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

// μέσα στη λούπα της λειτουργίας
Lib:= TPDFlib.Create;
try
  Lib.OnProgress:= Reporter.Progress;
  Started:= GetTickCount64;
  LoadCode:= Lib.LoadFromFile(InputFile, Password);
  ...

Ένα μηδέν είναι χειρότερο από έναν ανακριβή αριθμό, επειδή κάθε σύγκριση που χτίζετε πάνω του διαιρεί με αυτόν. Ο paired comparison runner μεταχειρίζεται κάθε arm με μηδενικό ελάχιστο ως αδιευκρίνιστο με την αιτιολογία «Zero duration prevents a meaningful ratio», που είναι η σωστή άρνηση, αλλά σημαίνει επίσης ότι τα timings του demo άφηναν κενό μέτρησης ακριβώς εκεί όπου ζουν τα σύντομα αρχεία. Το ίδιο demo εγκαθιστά και callback OnProgress, οπότε τα timings του περιλαμβάνουν overhead callback που μια καθαρή μέτρηση load/save δεν πρέπει να κουβαλά, και οι αρχειοθετημένοι αριθμοί του demo δεν είναι εναλλάξιμοι με τίποτα που μετριέται αργότερα

Μέτρηση LoadFromFile και SaveToFile με QueryPerformanceCounter

Η αφιερωμένη console probe, Tests/CorpusLoadSave.dpr, μετρά δύο λειτουργίες ανά αρχείο εισόδου με QueryPerformanceCounter: LoadFromFile συν ανάγνωση PageCount, και LoadFromFile συν PageCount συν SaveToFile. Κάθε λειτουργία παίρνει φρέσκο instance TPDFlib και κανένα progress callback, και ο constructor και destructor του instance κάθονται έξω από την μετρημένη περιοχή, όπως και η εγγραφή CSV και όλος ο έλεγχος εξόδου. Ο counter διαβάζεται αμέσως πριν τη φόρτωση και αμέσως μετά την τελευταία κλήση βιβλιοθήκης, και το LastErrorCode ανακτάται μόνο μετά τη δεύτερη ανάγνωση

Μετρημένη περιοχή της corpus probe του PDFlibPas: το QueryPerformanceCounter διαβάζεται αμέσως πριν το LoadFromFile και ξανά μετά την τελευταία κλήση βιβλιοθήκης, με PageCount και SaveToFile μέσα, ενώ το στήσιμο του instance, η εγγραφή CSV, ο έλεγχος εξόδου και η ανάκτηση error code μένουν όλα έξω από τη μετρημένη περιοχή
Τα raw ticks και η συχνότητα του counter καταγράφονται δίπλα στα παραγόμενα δευτερόλεπτα, οπότε ένα CAD σχέδιο που φορτώνει σε 8.888 ticks στα δέκα εκατομμύρια ticks ανά δευτερόλεπτο διατηρείται ως πραγματικά δεδομένα αντί να στρογγυλοποιηθεί στο μηδέν
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');

Η probe γράφει το raw πλήθος ticks και τη συχνότητα του counter δίπλα στα παραγόμενα δευτερόλεπτα, μορφοποιημένα με εννέα δεκαδικά ψηφία και σταθερό διαχωριστικό δεκαδικών ., οπότε ο καθένας μπορεί να ξαναϋπολογίσει το πηλίκο από το CSV αντί να το εμπιστευτεί. Στο FPC Win64 build το δείγμα CAD φορτώθηκε σε 8.888 ticks στα 10.000.000 ticks ανά δευτερόλεπτο, καταγεγραμμένο ως 0.000888800 δευτερόλεπτα — μια παρατήρηση που το παλιό timer θα στρογγυλοποιούσε στο μηδέν. Η probe σκόπιμα δεν κόβει σύντομες τιμές, δεν υποκαθιστά ελάχιστη διάρκεια, και δεν αφαιρεί εκτιμώμενο overhead timer, και γράφει και τις δύο γραμμές με μη μηδενικό exit code όταν αποτυγχάνει κλήση βιβλιοθήκης. Εννέα ψηφία όμως δεν είναι ακρίβεια: περισσότερη καταγεγραμμένη ακρίβεια δεν λέει τίποτα για την επαναληψιμότητα, και οι θορυβώδεις ή μηδενικές παρατηρήσεις εξακολουθούν να πρέπει να απορρίπτονται κατάντη

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

Τι κάνει μια σύγκριση timings load/save PDF αξιόπιστη;

Μια σύγκριση timings ανάμεσα σε δύο builds του PDF Library for Delphi είναι αξιόπιστη μόνο όταν σειρά εκκίνησης, συνθήκες εκκίνησης, και spread ελέγχονται και καταγράφονται όλα, γι' αυτό ο comparison runner προγραμματίζει τουλάχιστον τρία ζευγάρια σε σειρά A/B, B/A, A/B. Το να τρέχετε πάντα πρώτα τον baseline δίνει κρυφά στον candidate πιο ζεστό file cache και διαφορετική θερμική κατάσταση· η εναλλαγή σειράς απλώνει αυτό το bias και στα δύο arms αντί να το πιστώσει σε ένα. Πριν από κάθε arm ο runner κάνει hash ολόκληρου του αρχείου εισόδου με SHA-256, που και επαληθεύει ότι δεν άλλαξε τίποτα και προ-διαβάζει τα ίδια bytes για όποιο arm κι είναι, και ξανακάνει hash τα δύο executables και τα εργαλεία επικύρωσης μετά από κάθε run ώστε ένα ξαναχτισμένο binary δεν γλιστρά στη μέση μιας σειράς

Ο runner μετά δειγματοποιεί machine-wide CPU utilization μία φορά το δευτερόλεπτο και ξεκινά το arm μόνο όταν ένα δείγμα πέσει στο 25% ή κάτω, περιμένοντας το πολύ 30 δευτερόλεπτα πριν καταγράψει την προσπάθεια ως απορριφθείσα. Αυτή η πύλη ελέγχει τη συνθήκη εκκίνησης και τίποτα άλλο: δεν απομονώνει το μηχάνημα κατά το run, και κατάσταση τροφοδοσίας, thermal throttling, δουλειά στο παρασκήνιο, και OS caching μπορούν ακόμα να μετακινήσουν τους αριθμούς. Το δεύτερο φίλτρο λοιπόν είναι στατιστικό με την πιο απλή έννοια. Για κάθε λειτουργία ο runner υπολογίζει range δια median για το arm baseline, το arm candidate, και την κατανομή των ζευγαρωτών λόγων candidate/baseline, και αν οποιοδήποτε από τα τρία ξεπερνά το 0.15 το αποτέλεσμα επισημαίνεται noisy αντί να αναφερθεί ως εύρημα

Πύλες ζευγαρωτής σύγκρισης στο PDFlibPas: τρία ζευγάρια τρέχουν σε σειρά A/B, B/A, A/B με hashing εισόδου SHA-256 πριν από κάθε arm, μια πύλη εκκίνησης περιμένει CPU στο 25% ή κάτω, και spread range-to-median πάνω από 0.15 στο LoadFromFile ή στο arm load-plus-SaveToFile επισημαίνουν το run ως noisy
Η εναλλαγή σειράς εκκίνησης απλώνει το bias cache και θερμότητας και στα δύο arms, και ο έλεγχος same-binary δείχνει τι μπορεί να αποδείξει ένα τέτοιο στήσιμο: λόγοι κοντά στο 1.0 θεμελιώνουν επαναληψιμότητα, ποτέ ισχύ για speedup

Γιατί ένας έλεγχος same-binary αποδεικνύει επαναληψιμότητα, όχι ταχύτητα;

Ένας έλεγχος same-binary τρέχει πανομοιότυπα executables ως baseline και candidate, οπότε ένας λόγος κοντά στο 1.0 μπορεί μόνο να αποδείξει ότι το στήσιμο μέτρησης επαναλαμβάνει τον εαυτό του· δεν μπορεί ποτέ να δείξει ότι μια υλοποίηση έγινε γρηγορότερη. Ο πρώτος αυστηρός έλεγχος στις 2026-09-21 χρησιμοποίησε την high-resolution FPC Win64 probe απέναντι σε έναν εγκεκριμένο tagged guide 70 σελίδων, και και οι έξι εκκινήσεις απορρίφθηκαν επειδή τα δείγματα CPU κυμάνθηκαν από 26.5% σε 93.8%. Η αναφορά περιείχε αποτυχίες και κανένα σύνολο, που είναι ακριβώς το αποτέλεσμα που θέλετε όταν το μηχάνημα είναι απασχολημένο. Μια επανάληψη την ίδια μέρα με byte-πανομοιότυπες εισόδους, το ίδιο executable probe, και αμετάβλητα όρια δέχτηκε και τις έξι εκκινήσεις μέσα σε 3 δευτερόλεπτα· κάθε spread range-to-median κατέληξε ανάμεσα σε 0.019 και 0.054, και οι διάμεσοι των λόγων ήταν 1.0084 για LoadFromFile και 0.9872 για LoadFromFile + SaveToFile

Αυτό το ζευγάρι αριθμών θεμελιώνει ένα πιστοποιημένο παράθυρο παρατήρησης και τίποτα περισσότερο. Όταν τα δύο binaries διαφέρουν, ένα σταθερό run επισημαίνεται descriptive comparison, με τη ρητή σημείωση ότι οι λόγοι είναι παρατηρήσεις, όχι στατιστική σημαντικότητα ή ισχύς speedup. Η πειθαρχία έχει τη μεγαλύτερη σημασία όταν επικυρώνετε στοχευμένες βελτιώσεις όπως αυτές που περιγράφονται στο profiling του PDF Library for Delphi και αντικατάσταση hot paths με hash indexes: ένας profiler σας λέει πού πάει ο χρόνος, αλλά μόνο ένα ελεγχόμενο ζευγαρωτό run σε πραγματικά έγγραφα σας λέει αν η αλλαγή επέζησε της επαφής με ολόκληρο το pipeline. Ακόμα ένα όριο που αξίζει να ειπωθεί δυνατά — το normal-save περιλαμβάνει φόρτωση, και το peak working set που καταγράφει ο runner είναι process-wide, οπότε τίποτα από αυτό δεν είναι μνήμη που αποδίδεται μόνο στην αποθήκευση

Τρεις πύλες εξόδου και ένας πίνακας τεσσάρων compilers

Κανένα timing του PDF Library for Delphi δεν μετράει εκτός αν το αρχείο που παρήγαγε περάσει τρεις ανεξάρτητες πύλες, επειδή μια αποθήκευση που γράφει γρήγορα ένα χαλασμένο PDF δεν είναι ταχύτερη αποθήκευση. Το benchmark ελέγχει πρώτα ότι και οι δύο λειτουργίες επέστρεψαν 1 και ανέφεραν τον εγκεκριμένο αριθμό σελίδων, και μετά επικυρώνει το μοναδικό αποθηκευμένο PDF σε αυτή τη σειρά:

Τρεις πύλες εξόδου στο PDFlibPas: και οι δύο λειτουργίες πρέπει να επιστρέψουν 1 με τον εγκεκριμένο PageCount, ένας ανεξάρτητος checker πρέπει να περάσει το αποθηκευμένο αρχείο χωρίς warnings, κάθε σελίδα πρέπει να αποδώσει σε σύνολο per-page image SHA-256 που ταιριάζει την πηγή, και τα nonvisual semantics πρέπει να ταιριάζουν σε optional content και measurement δομές
Μια αποθήκευση που γράφει γρήγορα χαλασμένο PDF δεν είναι ταχύτερη αποθήκευση, οπότε ένα timing μετράει μόνο όταν δομή, rendering και nonvisual semantics συμφωνούν όλοι ότι η έξοδος είναι ακόμα το ίδιο έγγραφο
  • Δομή: ένας ανεξάρτητος PDF checker πρέπει να περάσει το αποθηκευμένο αρχείο χωρίς errors ή warnings
  • Rendering: κάθε σελίδα αποδίδεται στην προεπιλεγμένη της κατάσταση, και το σύνολο per-page image SHA-256 πρέπει να ταιριάζει ακριβώς το reference rendering της εγκεκριμένης πηγής
  • Nonvisual semantics: μια ξεχωριστή semantic σύγκριση απέναντι στην πηγή καλύπτει επιλεγμένες ιδιότητες που τα pixels δεν μπορούν να δείξουν, συμπεριλαμβανομένων δομών optional-content και measurement μέσα στο τεκμηριωμένο τους πεδίο

Με αυτές τις πύλες στη θέση τους, ο πλήρης τοπικός πίνακας corpus έτρεξε την probe σε FPC Win32, FPC Win64, Delphi Win32, και Delphi Win64 πάνω σε 12 εγκεκριμένα PDF με 1.612 σελίδες πηγής, δίνοντας 48 ζευγάρια sample/target και 6.448 επικυρωμένες σελίδες εξόδου χωρίς επιλεγμένες semantic διαφορές. Και οι 96 μετρήσεις λειτουργιών κράτησαν θετικές raw τιμές counter συνεπείς με τα αναφερόμενα δευτερόλεπτά τους, και αυτές οι τιμές σκόπιμα δεν συσσωρεύονται σε πίνακα ταχύτητας cross-compiler, επειδή ο πίνακας είναι λειτουργικό τεκμήριο και όχι ελεγχόμενη σύγκριση. Το μονοπάτι load/save επίσης δεν ισχυρίζεται ότι αποκωδικοποιεί κάθε embedded εικόνα, επικυρώνει υπογραφές, εκτελεί XFA, ή πιστοποιεί PDF/UA· αν θέλετε να κρίνετε rendering throughput αντί για κόστος load/save, οι περιορισμοί concurrency στο παράλληλο page rendering και thread safety στο PDF Library for Delphi είναι το καλύτερο σημείο εκκίνησης

Το πρακτικό συμπέρασμα είναι σύντομο: κρατήστε raw counters, εναλλάξτε τη σειρά, φράξτε την εκκίνηση, αρνηθείτε θορυβώδη spreads, και ποτέ μη μετρήσετε έξοδο που δεν έχετε επικυρώσει. Εκείνοι οι κανόνες είναι που επιτρέπουν στο PDF Library for Delphi να λέει «καμία μετρήσιμη αλλαγή» με την ίδια σιγουριά που λέει «γρηγορότερο», και η ίδια πηγή probe μεταγλωττίζεται αμετάβλητη σε Delphi και FPC για Win32 και Win64. Μπορείτε να δείτε τη βιβλιοθήκη, το load/save API της, και τους υποστηριζόμενους compilers στη σελίδα προϊόντος PDF Library for Delphi