مقاله فنی

بنچمارک صادقانه load/save PDF در Delphi: دروازه‌های نویز

یک بنچمارک صادقانهٔ load/save برای PDF Library for Delphi زمان LoadFromFile و SaveToFile را با QueryPerformanceCounter می‌گیرد، tickهای خام و فرکانس counter را نگه می‌دارد، baseline و candidate را در جفت‌های متناوب A/B و B/A و A/B اجرا می‌کند، تا وقتی بار CPU بالای 25% است از شروع خودداری می‌کند، هر نتیجه‌ای که پراکندگی range-به-median اش از 15% بگذرد را رد می‌کند و هر زمان‌گیری‌ای که PDF ذخیره‌شده‌اش اعتبارسنجی ساختاری یا رندر یا معنایی را از دست بدهد را دور می‌ریزد. این فهرست تا اولین باری که ادعای «20% سریع‌تر» در یک اجرای دوباره بخاری می‌شود شبیه بوروکراسی به نظر می‌رسد. آنچه در پی می‌آید این است که probe مخصوص کورپوس و runner مقایسه‌اش چطور به اینجا رسیدند، از جمله آن اجرایی که ماشین آن‌قدر شلوغ بود که هیچ چیز قابل‌اندازه‌گیری نبود و harness درست گفت که نمی‌شود

چرا یک بنچمارک PDF در Delphi صفر ثانیه گزارش می‌کند؟

یک بنچمارک بارگذاری PDF وقتی صفر ثانیه گزارش می‌دهد که تیک ساعتش از عملیاتی که اندازه می‌گیرد درشت‌تر باشد، و GetTickCount64 دقیقاً همین جور ساعتی است: میلی‌ثانیه برمی‌گرداند اما روی ویندوز فقط وقتی جلو می‌رود که وقفهٔ تایمر سیستم بیفتد، معمولاً هر 15.6 ms یک بار. پورت 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);
  ...

صفر از یک عدد بی‌دقت هم بدتر است، چون هر مقایسه‌ای که رویش بسازی تقسیم بر آن می‌شود. runner مقایسهٔ جفت‌شده هر بازویی با کمینهٔ صفر را بی‌نتیجه اعلام می‌کند با دلیل «Zero duration prevents a meaningful ratio»، که امتناع درستی است، اما یعنی زمان‌های دمو دقیقاً جایی که فایل‌های کوتاه زندگی می‌کنند یک شکاف اندازه‌گیری جا گذاشتند. همان دمو یک callback به نام OnProgress هم نصب می‌کند، پس زمان‌هایش سربار callback را هم حمل می‌کنند که یک اندازه‌گیری تمیز load/save نباید حمل کند، و اعداد دموی آرشیوشده با هیچ چیز اندازه‌گیری‌شدهٔ بعدی قابل‌تبادل نیستند

زمان‌گیری LoadFromFile و SaveToFile با QueryPerformanceCounter

probe کنسول مخصوص، یعنی Tests/CorpusLoadSave.dpr، به‌ازای هر فایل ورودی دو عملیات را با QueryPerformanceCounter اندازه می‌گیرد: LoadFromFile به‌علاوهٔ خواندن PageCount، و LoadFromFile به‌علاوهٔ PageCount به‌علاوهٔ SaveToFile. هر عملیات یک instance تازهٔ TPDFlib می‌گیرد و هیچ callback پیشرفتی ندارد، و constructor و destructor آن instance بیرون از ناحیهٔ زمان‌گیری می‌نشینند، همان‌طور که نوشتن CSV و همهٔ اعتبارسنجی خروجی. counter درست قبل از بارگذاری و درست بعد از آخرین فراخوانی کتابخانه خوانده می‌شود و LastErrorCode فقط بعد از خواندن دوم گرفته می‌شود

ناحیهٔ زمان‌گیری probe کورپوس در PDFlibPas: QueryPerformanceCounter درست قبل از LoadFromFile و باز هم بعد از آخرین فراخوانی کتابخانه خوانده می‌شود، PageCount و SaveToFile داخل آن‌اند، در حالی که آماده‌سازی instance، نوشتن CSV، اعتبارسنجی خروجی و گرفتن کد خطا همه بیرون از ناحیهٔ زمان‌گیری می‌مانند
tickهای خام و فرکانس counter کنار ثانیه‌های محاسبه‌شده ثبت می‌شوند، پس یک نقشهٔ CAD که در 8,888 تیک با سرعت ده میلیون تیک بر ثانیه بارگذاری می‌شود به‌عنوان دادهٔ واقعی حفظ می‌شود، نه اینکه به صفر گرد شود
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 شمار tick خام و فرکانس counter را کنار ثانیه‌های محاسبه‌شده با نُه رقم اعشار و جداکنندهٔ اعشار ثابت . می‌نویسد، پس هر کسی می‌تواند خارج قسمت را از CSV دوباره حساب کند به‌جای آنکه به آن اعتماد کند. در بیلد FPC Win64 نمونهٔ CAD در 8,888 تیک با 10,000,000 تیک بر ثانیه بارگذاری شد و به‌شکل 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));

چه چیزی یک مقایسهٔ زمان‌گیری load/save در PDF را قابل‌اعتماد می‌کند؟

مقایسهٔ زمان‌گیری بین دو بیلد از PDF Library for Delphi فقط وقتی قابل‌اعتماد است که ترتیب شروع، شرایط شروع و پراکندگی همگی کنترل و ثبت شده باشند، پس runner مقایسه دست‌کم سه جفت را به ترتیب A/B و B/A و A/B زمان‌بندی می‌کند. همیشه اول اجرا کردن baseline بی‌سروصدا به candidate یک file cache گرم‌تر و یک حالت حرارتی متفاوت هدیه می‌دهد؛ متناوب کردن ترتیب این سوگیری را بین هر دو بازو پخش می‌کند به‌جای آنکه به حساب یکی بنشیند. پیش از هر بازو runner فایل ورودی کامل را با SHA-256 هش می‌کند، که هم تأیید می‌کند هیچ چیز عوض نشده و هم همان بایت‌ها را برای هر دو بازو پیش‌خوانده، و بعد از هر اجرا دوباره دو فایل اجرایی و ابزارهای اعتبارسنجی را هش می‌کند تا یک باینری بازساخته‌شده نتواند وسط یک سری قاچاق شود

بعد runner در هر ثانیه یک بار مصرف CPU سراسر ماشین را نمونه‌برداری می‌کند و فقط وقتی نمونه‌ای به 25% یا زیرش رسید بازو را شروع می‌کند، با حداکثر 30 ثانیه انتظار پیش از آنکه تلاش را ردشده ثبت کند. آن گیت شرایط شروع را کنترل می‌کند و فقط همان: در حین اجرا ماشین را ایزوله نمی‌کند و حالت توان، throttling حرارتی، کار پس‌زمینه و کش سیستم‌عامل هنوز می‌توانند اعداد را جابه‌جا کنند. پس فیلتر دوم در ساده‌ترین معنایش آماری است. برای هر عملیات runner خارج قسمت range تقسیم بر median را برای بازوی baseline و بازوی candidate و توزیع نسبت‌های جفتی candidate/baseline حساب می‌کند و اگر هر یک از آن سه از 0.15 بگذرد، نتیجه به‌جای گزارش‌شدن به‌عنوان یافته، پرنویز برچسب می‌خورد

دروازه‌های مقایسهٔ جفت‌شده در PDFlibPas: سه جفت به ترتیب A/B و B/A و A/B اجرا می‌شوند با هش SHA-256 ورودی پیش از هر بازو، یک گیت شروع منتظر CPU با 25 درصد یا کمتر می‌ماند، و پراکندگی‌های range-به-median بالای 0.15 روی LoadFromFile یا بازوی load به‌علاوهٔ SaveToFile اجرا را پرنویز برچسب می‌زنند
متناوب کردن ترتیب شروع سوگیری cache و حرارت را بین هر دو بازو پخش می‌کند، و کنترل same-binary نشان می‌دهد چنین setupای چه چیزی را می‌تواند اثبات کند: نسبت‌های نزدیک 1.0 تکرارپذیری را ثابت می‌کنند، هرگز ادعای سریع‌تر بودن را

چرا کنترل same-binary تکرارپذیری را ثابت می‌کند، نه سرعت؟

یک کنترل same-binary فایل‌های اجرایی یکسان را به‌عنوان baseline و candidate اجرا می‌کند، پس نسبتی نزدیک 1.0 فقط می‌تواند ثابت کند setup اندازه‌گیری خودش را تکرار می‌کند؛ هرگز نمی‌تواند نشان دهد یک پیاده‌سازی سریع‌تر شده. اولین کنترل سخت‌گیرانه در 2026-09-21 probe با وضوح بالا FPC Win64 را روی یک راهنمای tagged اذعان‌شدهٔ 70 صفحه‌ای اجرا کرد و هر شش شروع به این دلیل رد شدند که نمونه‌های CPU بین 26.5% تا 93.8% بودند. گزارش حاوی شکست‌ها و بدون میانگین‌ها بود، که دقیقاً همان نتیجه‌ای است که وقتی ماشین شلوغ است می‌خواهی. تلاش دوباره در همان روز با ورودی‌های بایت‌به‌بایت یکسان، همان فایل اجرایی probe و آستانه‌های بدون تغییر، هر شش شروع را ظرف 3 ثانیه پذیرفت؛ همهٔ پراکندگی‌های range-به-median بین 0.019 و 0.054 ماندند و میانهٔ نسبت‌ها 1.0084 برای LoadFromFile و 0.9872 برای LoadFromFile + SaveToFile بود

آن جفت عدد فقط یک پنجرهٔ مشاهدهٔ مشروط ثابت می‌کند و بس. وقتی دو باینری متفاوت‌اند، یک اجرای پایدار مقایسهٔ توصیفی برچسب می‌خورد، با این یادداشت صریح که نسبت‌ها مشاهده‌اند، نه معناداری آماری و نه ادعای سریع‌تر بودن. این انضباط وقتی بیشترین اهمیت را دارد که بهینه‌سازی‌های هدفمند را راستی‌آزمایی می‌کنی، مثل آنچه در پروفایل PDF Library for Delphi و جایگزینی مسیرهای داغ با hash index آمده: profiler به تو می‌گوید زمان کجا می‌رود، اما فقط یک اجرای جفت‌شدهٔ کنترل‌شده روی اسناد واقعی می‌گوید آیا تغییر از برخورد با کل پایپ‌لاین جان سالم به در برد. یک مرز دیگر هم هست که ارزش گفتنش را دارد — normal-save بارگذاری را هم شامل می‌شود و peak working setای که runner ثبت می‌کند در سطح فرایند است، پس هیچ‌کدامش حافظه‌ای نیست که فقط به save نسبت داده شود

سه دروازهٔ خروجی و ماتریس چهار کامپایلری

هیچ زمان‌گیری‌ای از PDF Library for Delphi حساب نمی‌شود مگر اینکه فایلی که تولید کرده از سه دروازهٔ مستقل بگذرد، چون saveای که سریع یک PDF خراب بنویسد یک save سریع‌تر نیست. بنچمارک اول بررسی می‌کند هر دو عملیات مقدار 1 برگردانده‌اند و تعداد صفحات اذعان‌شده را گزارش کرده‌اند، بعد PDF ذخیره‌شدهٔ تکی را به این ترتیب اعتبارسنجی می‌کند:

سه دروازهٔ خروجی در PDFlibPas: هر دو عملیات باید با PageCount اذعان‌شده مقدار 1 برگردانند، یک checker مستقل باید فایل ذخیره‌شده را بدون هشدار قبول کند، هر صفحه باید به مجموعهٔ SHA-256 تصویر صفحه‌به‌صفحه‌ای رندر شود که با منبع بخواند، و معناشناسی غیربصری باید روی optional content و ساختارهای اندازه‌گیری بخواند
saveای که سریع یک PDF خراب بنویسد یک save سریع‌تر نیست، پس یک زمان‌گیری فقط وقتی حساب می‌شود که ساختار و رندر و معناشناسی غیربصری همه قبول کنند خروجی هنوز همان سند است
  • ساختار: یک checker مستقل PDF باید فایل ذخیره‌شده را بدون خطا و هشدار قبول کند
  • رندر: هر صفحه در حالت پیش‌فرضش رندر می‌شود و مجموعهٔ SHA-256 تصویر صفحه‌به‌صفحه باید دقیقاً با رندر مرجع منبع اذعان‌شده بخواند
  • معناشناسی غیربصری: یک مقایسهٔ معنایی جداگانه نسبت به منبع، ویژگی‌های انتخابی‌ای را پوشش می‌دهد که پیکسل‌ها نمی‌توانند نشانشان بدهند، از جمله ساختارهای optional-content و اندازه‌گیری در محدودهٔ مستندشده‌شان

با این دروازه‌ها سر جایشان، ماتریس کامل کورپوس محلی probe را روی FPC Win32 و FPC Win64 و Delphi Win32 و Delphi Win64 روی 12 PDF اذعان‌شده با 1,612 صفحهٔ منبع اجرا کرد؛ نتیجه 48 جفت sample/target و 6,448 صفحهٔ خروجی اعتبارسنجی‌شده بدون هیچ تفاوت معنایی انتخابی شد. هر 96 اندازه‌گیری عملیات مقادیر خام counter مثبتی داشتند که با ثانیه‌های گزارش‌شده‌شان سازگار بودند، و آن مقادیر عمداً در یک جدول سرعت بین-کامپایلری جمع نمی‌شوند، چون ماتریس شاهد کارکردی است نه یک مقایسهٔ کنترل‌شده. مسیر load/save هم ادعا نمی‌کند هر تصویر embedded را decode می‌کند یا امضاها را اعتبارسنجی می‌کند یا XFA را اجرا می‌کند یا PDF/UA را گواهی می‌کند؛ اگر باید به‌جای هزینهٔ load/save توان‌عملیاتی رندر را قضاوت کنی، محدودیت‌های همزمانی در رندر موازی صفحات و thread safety در PDF Library for Delphi نقطهٔ شروع بهتری است

جمع‌بندی عملی کوتاه است: counterهای خام را نگه دار، ترتیب را متناوب کن، شروع را گیت کن، پراکندگی‌های پرنویز را رد کن و هرگز خروجی‌ای را زمان‌گیری نکن که اعتبارسنجی نکرده‌ای. همین قواعدند که به PDF Library for Delphi اجازه می‌دهند «تغییر قابل‌اندازه‌گیری نیست» را با همان اطمینانِ «سریع‌تر» بگوید، و همان source صفحهٔ probe بدون تغییر روی Delphi و FPC برای Win32 و Win64 کامپایل می‌شود. می‌توانی کتابخانه، API مربوط به load/save و کامپایلرهای پشتیبانی‌شده را در صفحهٔ محصول PDF Library for Delphi مرور کنی