בנצ'מרק כן של טעינה ושמירה עבור PDF Library for Delphi מודד את LoadFromFile ו-SaveToFile עם QueryPerformanceCounter, שומר את ה-ticks הגולמיים ואת תדירות המונה, מריץ baseline ו-candidate בזוגות מתחלפים A/B, B/A, A/B, מסרב להתחיל כשעומס ה-CPU יושב מעל 25%, דוחה כל תוצאה שפיזור הטווח-לחציון שלה חורג מ-15%, וזורק כל מדידה שה-PDF שנשמר בה נכשל באימות מבני, רינדור או סמנטיקה. הרשימה הזאת נשמעת כמו בירוקרטיה עד הפעם הראשונה שבה טענה של "20% מהר יותר" מתאדה בריצה חוזרת. מה שבא להלן הוא איך ה-probe הייעודי לקורפוס וה-comparison runner שלו הגיעו לשם, כולל הריצה שבה המכונה הייתה פשוט עסוקה מדי מכדי למדוד משהו וה-harness אמר זאת בצדק
למה בנצ'מרק של PDF בדלפי מדווח על אפס שניות?
בנצ'מרק של טעינת PDF מדווח על אפס שניות כשהשעון שלו מתקתק בגסות רבה מן הפעולה שהוא מודד, ו-GetTickCount64 הוא בדיוק השעון הזה: הוא מחזיר מילישניות, אבל על Windows הוא מתקדם רק כשהפרעת הטיימר של המערכת יורה, בדרך כלל כל 15.6 ms. ה-port ל-FPC של הדמו של בנצ'מרק הקבצים הענקיים ב-PDF Library for Delphi השתמש בו כי TStopwatch לא זמין ב-toolchain ההוא, והוא רושם זמן שחלף בשלוש ספרות אחרי הנקודה. טעינה של שרטוט CAD קטן או מסמך tagged קצר מסתיימת בקלות בתוך צעד טיימר אחד, ולכן הדמו לפעמים הדפיס 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);
...
אפס הוא גרוע ממספר לא מדויק, כי כל השוואה שתבנו עליו מחלקת בו. ה-comparison runner המזווג מתייחס לכל זרוע עם מינימום אפס כלא חד-משמעית עם הסיבה "משך אפס מונע יחס בעל משמעות", וזו הסירוב הנכון, אבל זה גם אומר שמדידות הדמו השאירו פער מדידה בדיוק שם שבו חיים קבצים קצרים. אותו דמו גם מתקין callback של OnProgress, כך שהמדידות שלו כוללות תקורת callback שמדידת טעינה/שמירה נקייה לא צריכה לשאת, ומספרי דמו בארכיון אינם ניתנים להחלפה עם דבר שנמדד אחר כך
מדידת LoadFromFile ו-SaveToFile עם QueryPerformanceCounter
ה-probe הייעודי לקונסולה, Tests/CorpusLoadSave.dpr, מודד שתי פעולות לכל קובץ קלט עם QueryPerformanceCounter: LoadFromFile בתוספת קריאת PageCount, ו-LoadFromFile בתוספת PageCount בתוספת SaveToFile. כל פעולה מקבלת מופע TPDFlib טרי ובלי callback של התקדמות, וה-constructor וה-destructor של המופע יושבים מחוץ לאזור המתוזמן, וכך גם כתיבת ה-CSV וכל אימות הפלט. את המונה קוראים מיד לפני הטעינה ומיד אחרי הקריאה האחרונה לספרייה, ואת LastErrorCode שולפים רק אחרי הקריאה השנייה
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 כותב את מספר ה-ticks הגולמי ואת תדירות המונה לצד השניות הנגזרות, בפורמט של תשע ספרות אחרי הנקודה עם מפריד עשרוני קבוע ., כך שכל אחד יכול לחשב את המנה מה-CSV בעצמו במקום לסמוך עליו. ב-build של FPC Win64 הדגימה של ה-CAD נטענה ב-8,888 ticks בקצב של 10,000,000 ticks בשנייה, נרשמה כ-0.000888800 שניות — תצפית שהטיימר הישן היה מעגל לאפס. ה-probe במכוון לא קוצץ ערכים קצרים, לא מחליף במשך מינימום ולא מחסיר תקורת טיימר משוערת, והוא עדיין כותב את שתי השורות עם קוד יציאה שאינו אפס כשקריאה לספרייה נכשלת. תשע ספרות אינן דיוק, לעומת זאת: דיוק רישום גדול יותר לא אומר דבר על חזרתיות, ותצפיות רועשות או אפס עדיין צריכות להידחות במורד הזרם
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));
מה הופך השוואת מדידות טעינה ושמירה של PDF לאמינה?
השוואת מדידות בין שני builds של PDF Library for Delphi היא אמינה רק כשסדר ההתחלה, תנאי ההתחלה והפיזור כולם נשלטים ונרשמים, ולכן ה-comparison runner קובע לפחות שלושה זוגות בסדר A/B, B/A, A/B. להריץ את ה-baseline תמיד ראשון מעביר בשקט ל-candidate file cache חם יותר ומצב תרמי אחר; החלפת הסדר מפזרת את ההטיה הזאת על שתי הזרועות במקום לזקוף אותה לאחת. לפני כל זרוע ה-runner מחשב hash של SHA-256 על קובץ הקלט המלא, מה שגם מאמת ששום דבר לא השתנה וגם קורא מראש את אותם בתים עבור כל זרוע, והוא מחשב hash שוב של שני קבצי ההרצה ושל כלי האימות אחרי כל ריצה, כך ש-binary שנבנה מחדש לא יוכל להתחפר לאמצע סדרה
אחר כך ה-runner דוגם את ניצולת ה-CPU ברמת המכונה פעם בשנייה ומתחיל את הזרוע רק כשדגימה יורדת ל-25% או מתחת, בהמתנה של 30 שניות לכל היותר לפני שהוא רושם את הניסיון כנדחה. השער הזה שולט בתנאי ההתחלה ובשום דבר אחר: הוא לא מבודד את המכונה במהלך הריצה, ומצב צריכת חשמל, thermal throttling, עבודת רקע ו-caching של מערכת ההפעלה עדיין יכולים להזיז את המספרים. לכן המסנן השני הוא סטטיסטי במובן הפשוט ביותר. עבור כל פעולה ה-runner מחשב טווח חלקי חציון עבור זרוע ה-baseline, זרוע ה-candidate והתפלגות היחסים המזווגים candidate/baseline, ואם אחד מהשלושה חורג מ-0.15 התוצאה מסומנת כרועשת במקום להירשם כממצא
למה בקרת same-binary מוכיחה חזרתיות ולא מהירות?
בקרת same-binary מריצה קבצי הרצה זהים כ-baseline וכ-candidate, ולכן יחס קרוב ל-1.0 יכול רק להוכיח שתצורת המדידה חוזרת על עצמה; הוא לעולם לא יוכל להראות שמימוש כלשהו נעשה מהיר יותר. הבקרה המחמירה הראשונה ב-2026-09-21 השתמשה ב-probe ברזולוציה גבוהה של FPC Win64 מול מדריך tagged מאושר בן 70 עמודים, וכל שש ההתחלות נדחו כי דגימות ה-CPU נעו בין 26.5% ל-93.8%. הדוח הכיל כשלים ולא מאגדים, וזה בדיוק התוצאה שרוצים כשהמכונה עסוקה. ניסיון חוזר באותו יום עם קלטים זהים בית-אחר-בית, אותו קובץ הרצה של ה-probe וספים ללא שינוי קיבל את כל שש ההתחלות תוך 3 שניות; כל פיזור טווח-לחציון נחת בין 0.019 ל-0.054, וחציוני היחסים היו 1.0084 עבור LoadFromFile ו-0.9872 עבור LoadFromFile + SaveToFile
זוג המספרים הזה מבסס חלון תצפית מוגבל ולא יותר מזה. כששני ה-binaries שונים, ריצה יציבה מסומנת כהשוואה תיאורית, עם ההערה המפורשת שהיחסים הם תצפיות, לא מובהקות סטטיסטית ולא טענת האצה. המשמעת הזאת חשובה בעיקר כשמאמתים אופטימיזציות ממוקדות כמו אלה שמתוארות בפרופיילינג של PDF Library for Delphi והחלפת נתיבים חמים באינדקסי hash: profiler אומר לכם לאן הזמן הולך, אבל רק ריצה מזווגת מבוקרת על מסמכים אמיתיים אומרת לכם אם השינוי שרד מגע עם כל ה-pipeline. גבול אחד נוסף ששווה לומר בקול — normal-save כולל טעינה, וזיכרון העבודה השיא שה-runner רושם הוא ברמת התהליך כולו, כך שאף חלק ממנו אינו זיכרון שאפשר לייחס לשמירה בלבד
שלושה שערי פלט ומטריצה של ארבעה מהדרים
שום מדידה של PDF Library for Delphi לא נחשבת אלא אם הקובץ שהפיקה עובר שלושה שערים בלתי תלויים, כי שמירה שכותבת PDF שבור במהירות איננה שמירה מהירה יותר. הבנצ'מרק בודק קודם ששתי הפעולות החזירו 1 ודיווחו על מספר העמודים המאושר, ואז מאמת את ה-PDF היחיד שנשמר בסדר הזה:
- מבנה: בודק PDF בלתי תלוי חייב לעבור את הקובץ הנשמר בלי שגיאות או אזהרות
- רינדור: כל עמוד מרונדר במצב ברירת המחדל שלו, וקבוצת ה-SHA-256 של התמונות פר-עמוד חייבת להתאים במדויק לרינדור הייחוס של המקור המאושר
- סמנטיקה בלתי נראית: השוואה סמנטית נפרדת מול המקור מכסה תכונות נבחרות שפיקסלים לא יכולים להציג, כולל מבני optional-content ומדידה בתוך ההיקף המתועד שלהם
עם השערים האלה במקומם, מטריצת הקורפוס המקומית המלאה הריצה את ה-probe על FPC Win32, FPC Win64, Delphi Win32 ו-Delphi Win64 על פני 12 PDF מאושרים עם 1,612 עמודי מקור, ונתנה 48 זוגות דגימה/יעד ו-6,448 עמודי פלט מאומתים בלי הבדלים סמנטיים נבחרים. כל 96 המדידות של הפעולות שמרו על ערכי מונה גולמיים חיוביים עקביים עם השניות שדיווחו, וערכים אלה במכוון אינם מאוגדים לטבלת מהירות בין-מהדרים, כי המטריצה היא ראיה פונקציונלית ולא השוואה מבוקרת. נתיב הטעינה/שמירה גם לא טוען לפענח כל תמונה מוטמעת, לאמת חתימות, להריץ XFA או לאשר PDF/UA; אם אתם צריכים לשפוט תפוקת רינדור ולא עלות טעינה/שמירה, מגבלות המקביליות ברינדור עמודים במקביל ו-thread safety ב-PDF Library for Delphi הן נקודת הפתיחה הטובה יותר
המסקנה המעשית קצרה: שומרים על מונים גולמיים, מחליפים את הסדר, שמים שער על ההתחלה, מסרבים לפיזורים רועשים, ולעולם לא מודדים פלט שלא אימתתם. הכללים האלה הם מה שמאפשר ל-PDF Library for Delphi לומר "שינוי בלתי נמדד" באותה ביטחון כמו "מהר יותר", ואותו קוד מקור של ה-probe מתקמפל ללא שינוי על Delphi ו-FPC עבור Win32 ו-Win64. אפשר לעיין בספרייה, ב-API שלה לטעינה ושמירה ובמהדרים הנתמכים בעמוד המוצר של PDF Library for Delphi