Bài viết kỹ thuật

Đo nạp/lưu PDF trung thực trong Delphi: cổng lọc nhiễu

Một benchmark nạp/lưu trung thực cho PDF Library for Delphi đo LoadFromFile và SaveToFile bằng QueryPerformanceCounter, giữ nguyên raw tick và tần số counter, chạy baseline và candidate theo từng cặp A/B, B/A, A/B xen kẽ, từ chối khởi động khi tải CPU đang trên 25%, loại mọi kết quả có độ rộng range-to-median vượt 15%, và vứt bỏ mọi phép đo mà tệp PDF lưu ra không qua được xác thực cấu trúc, kết xuất hay ngữ nghĩa. Danh sách đó đọc lên cứ như giấy tờ hành chính, cho tới lần đầu tiên một tuyên bố "nhanh hơn 20%" bay hơi khi chạy lại. Phần dưới đây là cách dedicated corpus probe cùng comparison runner của nó đi tới chỗ đó, trong đó có ca chạy mà máy bận túi túi chẳng đo nổi thứ gì và harness đã nói đúng như vậy

Vì sao benchmark PDF Delphi báo 0 giây?

Benchmark nạp PDF báo 0 giây khi đồng hồ của nó nhịp thô hơn cả phép toán nó định đo, và GetTickCount64 đúng là loại đồng hồ ấy: nó trả về mili giây, nhưng trên Windows chỉ nhích lên khi ngắt timer hệ thống bắn ra, thường là mỗi 15.6 ms. Bản port FPC của demo benchmark tệp khổng lồ trong PDF Library for Delphi dùng nó vì TStopwatch không có sẵn trong toolchain đó, và nó ghi thời gian trôi qua tới ba chữ số thập phân. Nạp một bản vẽ CAD nhỏ hay một tài liệu tagged ngắn kết thúc ngay bên trong một nhịp timer, nên demo thỉnh thoảng in 0.000 cho một lần nạp mà rõ ràng đã làm việc thật

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

// bên trong vòng lặp thao tác
Lib:= TPDFlib.Create;
try
  Lib.OnProgress:= Reporter.Progress;
  Started:= GetTickCount64;
  LoadCode:= Lib.LoadFromFile(InputFile, Password);
  ...

Một con số 0 còn tệ hơn cả một con số thiếu chính xác, vì mọi phép so sánh bạn xây trên nó đều phải chia cho nó. Comparison runner theo cặp coi bất kỳ nhánh nào có giá trị nhỏ nhất bằng 0 là không kết luận được, kèm lý do "Zero duration prevents a meaningful ratio", một lời từ chối đúng đắn, nhưng nó cũng có nghĩa là các phép đo của demo để hở một khoảng trống đúng ở chỗ những tệp ngắn cư ngụ. Demo đó còn gắn thêm callback OnProgress, nên các phép đo của nó dính cả chi phí callback mà một phép đo nạp/lưu sạch không đáng mang, và các con số demo lưu trong kho không thể hoán đổi với bất kỳ thứ gì đo sau này

Đo LoadFromFile và SaveToFile bằng QueryPerformanceCounter

Console probe chuyên dụng, Tests/CorpusLoadSave.dpr, đo hai phép toán cho mỗi tệp đầu vào bằng QueryPerformanceCounter: LoadFromFile cộng với việc đọc PageCount, và LoadFromFile cộng PageCount cộng SaveToFile. Mỗi phép toán nhận một instance TPDFlib mới tinh và không có progress callback, còn constructor và destructor của instance nằm ngoài vùng đo thời gian, tương tự như việc ghi CSV và toàn bộ xác thực output. Counter được đọc ngay trước lúc nạp và ngay sau lệnh gọi thư viện cuối cùng, còn LastErrorCode chỉ được lấy sau lần đọc thứ hai

Vùng đo thời gian của corpus probe PDFlibPas: QueryPerformanceCounter được đọc ngay trước LoadFromFile và lại ngay sau lệnh gọi thư viện cuối cùng, với PageCount và SaveToFile bên trong, trong khi dựng instance, ghi CSV, xác thực output và lấy error code đều nằm ngoài vùng đo
Raw tick và tần số counter được ghi cạnh số giây suy ra, nên một bản vẽ CAD nạp hết 8.888 tick với tốc độ mười triệu tick mỗi giây được giữ lại như dữ liệu thật thay vì bị làm tròn thành 0
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 ghi raw tick count và tần số counter cạnh số giây suy ra, định dạng với chín chữ số thập phân và dấu thập phân . cố định, để ai cũng có thể tự tính lại thương số từ CSV thay vì tin mù quáng. Trên bản build FPC Win64, mẫu CAD nạp hết 8.888 tick với 10.000.000 tick mỗi giây, được ghi thành 0.000888800 giây — một quan sát mà timer cũ sẽ làm tròn về 0. Probe chủ động không cắt giá trị ngắn, không thay bằng thời lượng tối thiểu, cũng không trừ đi ước lượng chi phí timer, và nó vẫn ghi cả hai dòng với exit code khác 0 khi một lệnh gọi thư viện thất bại. Nhưng chín chữ số không phải là độ chính xác: nhiều chữ số được ghi lại chẳng nói lên điều gì về tính lặp lại, và các quan sát nhiễu hay bằng 0 vẫn phải bị loại ở hạ nguồn

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

Điều gì khiến một phép so sánh thời gian nạp/lưu PDF đáng tin?

Một phép so sánh thời gian giữa hai bản build của PDF Library for Delphi chỉ đáng tin khi thứ tự khởi động, điều kiện khởi động và độ rộng được kiểm soát lẫn ghi lại đầy đủ, vì thế comparison runner xếp ít nhất ba cặp theo thứ tự A/B, B/A, A/B. Luôn chạy baseline trước âm thầm trao cho candidate một file cache ấm hơn và một trạng thái nhiệt khác; xen kẽ thứ tự phân tán bias đó cho cả hai nhánh thay vì ghi công cho một nhánh. Trước mỗi nhánh, runner hash toàn bộ tệp đầu vào bằng SHA-256, vừa xác minh chẳng có gì thay đổi vừa đọc trước cùng một lô byte cho cả hai nhánh, và nó hash lại hai executable cùng các công cụ xác thực sau mỗi lần chạy để một binary build lại không thể lẻn vào giữa một chuỗi phép đo

Runner sau đó lấy mẫu mức sử dụng CPU toàn máy mỗi giây một lần và chỉ khởi động nhánh khi một mẫu rơi xuống 25% hoặc thấp hơn, chờ tối đa 30 giây trước khi ghi nhận lần thử là bị loại. Cổng này kiểm soát điều kiện khởi động và chẳng gì khác: nó không cô lập máy trong lúc chạy, và trạng thái nguồn, thermal throttling, công việc nền cùng OS caching vẫn có thể lay chuyển các con số. Vì thế bộ lọc thứ hai mang tính thống kê theo nghĩa mộc mạc nhất. Với mỗi phép toán, runner tính range chia median cho nhánh baseline, nhánh candidate, và phân bố các tỉ lệ candidate/baseline theo cặp, và nếu bất kỳ giá trị nào trong ba vượt 0.15, kết quả bị dán nhãn nhiễu thay vì được báo như một phát hiện

Các cổng so sánh theo cặp trong PDFlibPas: ba cặp chạy theo thứ tự A/B, B/A, A/B với hash SHA-256 đầu vào trước mỗi nhánh, cổng khởi động chờ CPU xuống 25 phần trăm trở xuống, và độ rộng range-to-median vượt 0.15 trên LoadFromFile hay nhánh nạp cộng SaveToFile sẽ gán nhãn nhiễu cho lần chạy
Xen kẽ thứ tự khởi động phân tán bias cache và nhiệt cho cả hai nhánh, và control cùng một binary cho thấy một thiết lập như vậy chứng minh được điều gì: tỉ số gần 1.0 khẳng định tính lặp lại, chưa bao giờ là tuyên bố nhanh hơn

Vì sao control cùng một binary chứng minh tính lặp lại, không phải tốc độ?

Một control cùng binary chạy hai executable giống hệt nhau làm baseline và candidate, nên tỉ số gần 1.0 chỉ chứng minh được rằng thiết lập đo lường lặp lại được chính nó; nó chưa bao giờ cho thấy một bản cài đặt nhanh hơn. Control nghiêm ngặt đầu tiên ngày 2026-09-21 dùng probe FPC Win64 độ phân giải cao trên một tài liệu tagged 70 trang đã được chấp nhận, và cả sáu lần khởi động đều bị loại vì các mẫu CPU trải từ 26.5% đến 93.8%. Báo cáo chỉ có thất bại và không có số tổng hợp, chính là kết quả bạn muốn khi máy đang bận. Một lần chạy lại trong cùng ngày, với đầu vào giống hệt từng byte, cùng executable probe và ngưỡng không đổi, chấp nhận cả sáu lần khởi động trong vòng 3 giây; mọi độ rộng range-to-median nằm giữa 0.019 và 0.054, và median tỉ số là 1.0084 cho LoadFromFile và 0.9872 cho LoadFromFile + SaveToFile

Cặp số đó chỉ dựng nên một cửa sổ quan sát có điều kiện chứ không hơn. Khi hai binary khác nhau, một lần chạy ổn định được dán nhãn so sánh mô tả, kèm ghi chú rõ ràng rằng các tỉ số là quan sát, không phải ý nghĩa thống kê hay tuyên bố nhanh hơn. Kỷ luật này quý nhất khi bạn đang xác thực các tối ưu nhắm đích như những gì bài profiling PDF Library for Delphi và thay thế hot path bằng hash index mô tả: profiler cho bạn biết thời gian đi đâu, nhưng chỉ một lần chạy theo cặp có kiểm soát trên tài liệu thật mới cho biết thay đổi có sống sót qua toàn bộ pipeline hay không. Một ranh giới nữa đáng nói ra — normal-save bao gồm cả việc nạp, và peak working set mà runner ghi lại là của toàn tiến trình, nên chẳng byte nào trong đó gán được riêng cho việc lưu

Ba cổng output và ma trận bốn compiler

Không phép đo thời gian nào của PDF Library for Delphi được tính nếu tệp nó tạo ra không qua được ba cổng độc lập, vì một lần lưu viết nhanh một PDF hỏng không phải là một lần lưu nhanh hơn. Benchmark đầu tiên kiểm tra cả hai phép toán trả về 1 và báo đúng số trang đã chấp nhận, rồi xác thực tệp PDF duy nhất vừa lưu theo thứ tự này:

Ba cổng output trong PDFlibPas: cả hai phép toán phải trả về 1 cùng PageCount đã chấp nhận, một checker độc lập phải cho qua tệp đã lưu mà không cảnh báo, mọi trang phải kết xuất ra bộ SHA-256 ảnh từng trang khớp với nguồn, và ngữ nghĩa phi thị giác phải khớp trên optional content lẫn cấu trúc đo lường
Một lần lưu viết nhanh một PDF hỏng không phải là lưu nhanh hơn, nên phép đo thời gian chỉ được tính khi cấu trúc, kết xuất và ngữ nghĩa phi thị giác cùng công nhận output vẫn là tài liệu đó
  • Cấu trúc: một PDF checker độc lập phải cho qua tệp đã lưu mà không có lỗi hay cảnh báo
  • Kết xuất: mọi trang được kết xuất ở trạng thái mặc định, và bộ SHA-256 ảnh từng trang phải khớp chính xác với bản kết xuất tham chiếu của nguồn đã chấp nhận
  • Ngữ nghĩa phi thị giác: một phép so sánh ngữ nghĩa riêng với nguồn bao trùm các thuộc tính chọn lọc mà pixel không thể hiện được, gồm optional-content và các cấu trúc đo lường trong phạm vi đã được ghi chép của chúng

Với các cổng đó tại chỗ, ma trận corpus cục bộ đầy đủ đã chạy probe trên FPC Win32, FPC Win64, Delphi Win32 và Delphi Win64 qua 12 tệp PDF đã chấp nhận với 1.612 trang nguồn, cho ra 48 cặp mẫu/đích và 6.448 trang output đã xác thực mà không có khác biệt ngữ nghĩa nào được chọn lọc. Cả 96 phép đo thao tác đều giữ giá trị raw counter dương khớp với số giây đã báo, và các giá trị đó bị chủ động không tổng hợp thành bảng tốc độ chéo compiler, vì ma trận này là bằng chứng chức năng chứ không phải một phép so sánh có kiểm soát. Đường nạp/lưu cũng không nhận decode mọi ảnh nhúng, xác thực chữ ký, chạy XFA hay chứng nhận PDF/UA; nếu bạn cần đánh giá throughput kết xuất thay vì chi phí nạp/lưu, các ràng buộc concurrency trong bài kết xuất trang song song và thread safety trong PDF Library for Delphi là điểm khởi đầu phù hợp hơn

Bài học thực dụng ngắn gọn: giữ raw counter, xen kẽ thứ tự, đặt cổng cho lúc khởi động, từ chối độ rộng nhiễu, và đừng bao giờ đo thời gian một output chưa được xác thực. Chính những luật đó giúp PDF Library for Delphi nói "không có thay đổi đo được" tự tin như nói "nhanh hơn", và cùng một mã nguồn probe compile không đổi trên Delphi lẫn FPC cho Win32 và Win64. Bạn có thể xem lại thư viện, load/save API của nó và các compiler được hỗ trợ trên trang sản phẩm PDF Library for Delphi