技術記事

Delphiの正直なPDF読み書きベンチマーク:ノイズゲートの作り方

PDF Library for Delphiの正直な読み書きベンチマークは、LoadFromFileとSaveToFileをQueryPerformanceCounterで計測し、生のカウンターティックとカウンター周波数を保持し、ベースラインと候補をA/B、B/A、A/Bと交互のペアで実行し、CPU負荷が25%を超えている間は開始を拒み、範囲対中央値のスプレッドが15%を超える結果は棄却し、保存したPDFが構造・描画・意味論の検証に落ちた計測値はすべて捨てます。このリストだけ読むと官僚主義に見えますが、「20%速くなった」という主張が再実行で消え飛んだ最初の瞬間から、そうは思えなくなります。以下では、専用のコーパスプローブと比較ランナーがどうやってここに至ったかを扱います。何も測れないほどマシンが忙しかった実行を、ハーネスが正しくそう判定した話も含めて

DelphiのPDFベンチマークが0秒を報告するのはなぜか

PDFの読み込みベンチマークが0秒を報告するのは、時計の目盛りが測定対象より粗いときです。GetTickCount64はまさにその種の時計で、ミリ秒を返しますが、Windowsではシステムタイマー割り込みが飛んだときにしか進みません。よくあるのは15.6 ms間隔です。PDF Library for Delphiの巨大ファイルベンチマークデモのFPC移植版はこれを使っていました。TStopwatchがそのツールチェーンでは使えないからです。そして経過時間を小数点以下3桁で記録します。小さなCAD図面や短いタグ付き文書の読み込みは1つのタイマーステップよりずっと内側で終わるので、デモは明らかに実際の処理をした読み込みに対して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);
  ...

ゼロは精度の悪い数値より厄介です。それを土台に築いた比較はすべて、ゼロで割ることになるからです。ペア比較ランナーは最小値がゼロのアームを、理由「Zero duration prevents a meaningful ratio」付きで決着不能として扱います。これ自体は正しい拒否ですが、短いファイルが住む領域にちょうど計測の空白を残したことも意味します。同じデモはOnProgressコールバックも組み込むので、その計測値にはクリーンな読み書き計測が背負うべきでないコールバックのオーバーヘッドが含まれ、アーカイブされたデモの数値は後で測ったどんな数値とも交換できません

QueryPerformanceCounterでLoadFromFileとSaveToFileを計測する

専用のコンソールプローブであるTests/CorpusLoadSave.dprは、入力ファイルごとに2つの操作をQueryPerformanceCounterで測ります。LoadFromFile+PageCountの読み取り、そしてLoadFromFile+PageCount+SaveToFileの2つです。各操作には新しいTPDFlibインスタンスが与えられ、進捗コールバックは付けません。インスタンスのコンストラクターとデストラクターは計測領域の外に置かれ、CSVの書き出しと出力検証もすべて同じく外です。カウンターはロードの直前と、最後のライブラリー呼び出しの直後に読み、LastErrorCodeは2回目の読み取りの後にだけ取得します

PDFlibPasコーパスプローブの計測領域。QueryPerformanceCounterはLoadFromFileの直前と最後のライブラリー呼び出しの直後に読まれ、PageCountとSaveToFileは内側にあり、インスタンスのセットアップ、CSV書き出し、出力検証、エラーコード取得はすべて計測領域の外に置かれます
生のティック数とカウンター周波数は導出した秒数の隣に記録されるので、1秒あたり1,000万ティックで8,888ティックのCAD図面はゼロに丸められず、実データのまま保存されます
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');

プローブは生のティック数とカウンター周波数を、導出した秒数の隣に書き出します。小数点以下9桁、固定の.小数点区切りで整形され、誰でもCSVから商を計算し直せるので、数値を鵜呑みにする必要がありません。FPC Win64ビルドでは、CADサンプルは1秒あたり10,000,000ティックの時計で8,888ティック、つまり0.000888800秒として記録されました。旧タイマーならゼロへ丸めていた観測です。プローブは意図的に、短い値の切り詰めも、最小時間の代用も、推定タイマーオーバーヘッドの差し引きもしません。ライブラリー呼び出しが失敗しても、両方の行を書いて0以外の終了コードで終わります。ただし9桁は精度ではありません。記録された精度が増えても再現性については何も言えず、ノイズだらけの観測やゼロの観測は、下流で棄却されなければなりません

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の読み書き計測比較を信頼できるものにする条件

PDF Library for Delphiの2つのビルド間の計測比較が信頼できるのは、開始順序と開始条件とスプレッドがすべて制御され、記録されているときだけです。そこで比較ランナーは少なくとも3ペアをA/B、B/A、A/Bの順に組んで実行します。常にベースラインを先に走らせていると、候補に暖まったファイルキャッシュと違う熱状態を黙って渡すことになります。順序を交互にすれば、そのバイアスは一方の功績にされる代わりに、両アームに分散されます。各アームの前に、ランナーは入力ファイル全体をSHA-256でハッシュします。これで何も変わっていないことを確認できるうえ、どちらのアームにも同じバイト列を先読みさせます。そして2つの実行ファイルと検証ツールは、実行のたびに再ハッシュされるので、作り直したバイナリがシリーズの途中に紛れ込むことはありません

そのうえでランナーはマシン全体のCPU使用率を1秒に1回サンプリングし、サンプルが25%以下に落ちたときだけアームを開始します。30秒待ってダメなら、その試行は棄却として記録されます。このゲートが制御するのは開始条件だけで、それ以外の何者でもありません。実行中にマシンを占有するわけではなく、電源状態、サーマルスロットリング、バックグラウンドの仕事、OSのキャッシングは今なお数値を動かせます。だから2番目のフィルターは、最も素朴な意味で統計的なのです。各操作について、ランナーはベースラインアーム、候補アーム、そしてペアになった候補/ベースライン比の分布それぞれに対して範囲÷中央値を計算し、3つのどれかが0.15を超えたら、その結果は知見として報告される代わりにnoisyのラベルを貼られます

PDFlibPasのペア比較ゲート。3ペアがA/B、B/A、A/Bの順で実行され、各アームの前にSHA-256で入力をハッシュし、開始ゲートはCPUが25%以下になるのを待ち、LoadFromFileまたはロード+SaveToFileのアームで範囲対中央値のスプレッドが0.15を超えると、その実行はnoisyとラベル付けされます
開始順序を交互にすると、キャッシュと熱のバイアスは両アームに分散されます。same-binaryコントロールが示すのは、この種のセットアップが証明できる範囲です。1.0近くの比率が確立するのは再現性であって、高速化の主張ではありません

same-binaryコントロールが証明するのは再現性であって速度でないのはなぜか

same-binaryコントロールは、同一の実行ファイルをベースラインと候補として走らせます。だから1.0近くの比率が証明できるのは、計測のセットアップが自分自身を再現したことだけです。実装が速くなったことを示すことは決してありません。2026-09-21の最初の厳格コントロールでは、高解像度のFPC Win64プローブを、受理済みの70ページのタグ付きガイドに使いました。CPUサンプルが26.5%から93.8%まで振れたため、6回の開始はすべて棄却です。レポートには失敗だけが並び、集計はありませんでした。マシンが忙しいときに望ましい結果はまさにこれです。同日の再試行では、バイト単位で同一の入力、同じプローブ実行ファイル、変えていない閾値で、6回の開始はすべて3秒以内に受理されました。範囲対中央値のスプレッドはすべて0.019から0.054の間に収まり、比率の中央値はLoadFromFileが1.0084、LoadFromFile + SaveToFileが0.9872でした

この2つの数値が確立するのは、条件付きの観測窓であって、それ以上ではありません。2つのバイナリが異なるとき、安定した実行は記述的比較のラベルを貼られ、比率は観測であって統計的有意性でも高速化の主張でもない、という但し書きが明示的に付きます。この規律が最も効いてくるのは、PDF Library for Delphiのプロファイリングとホットパスのハッシュインデックス化で述べたような、狙いを定めた最適化を検証するときです。プロファイラーは時間がどこへ行くかを教えてくれますが、変更がパイプライン全体との接触に耐えたかどうかは、実文書での統制されたペア実行だけが教えてくれます。もう1つ、声に出して言っておきたい境界があります。normal-saveにはロードが含まれ、ランナーが記録するピークワーキングセットはプロセス全体のものなので、保存だけに起因するメモリーはそこにありません

3つの出力ゲートと4コンパイラーのマトリクス

PDF Library for Delphiの計測値は、作られたファイルが3つの独立したゲートを通らなければ1つとして数えません。壊れたPDFを素早く書く保存は、速い保存ではないからです。ベンチマークはまず、両方の操作が1を返し受理済みのページ数を報告したことを確認し、それから保存された1つのPDFをこの順に検証します:

PDFlibPasの3つの出力ゲート。両方の操作が受理済みのPageCountとともに1を返すこと、独立したチェッカーが警告なしで保存ファイルを通すこと、全ページがソースと一致するページ単位の画像SHA-256集合へ描画されること、そしてoptional contentとmeasurement構造で非視覚の意味論が一致することが求められます
壊れたPDFを素早く書く保存は速い保存ではないので、計測値が数えられるのは、構造と描画と非視覚の意味論がすべて、出力がまだ同じ文書だと一致したときだけです
  • 構造:独立したPDFチェッカーが、エラーも警告もなしで保存ファイルを通すこと
  • 描画:全ページが既定の状態で描画され、ページ単位の画像SHA-256集合が、受理済みソースの参照描画と正確に一致すること
  • 非視覚の意味論:ソースとの別個の意味比較が、ピクセルでは見えない選ばれた属性をカバーします。optional-contentとmeasurement構造も、文書化された範囲内で対象です

これらのゲートを備えたうえで、ローカルコーパスの完全なマトリクスが、FPC Win32、FPC Win64、Delphi Win32、Delphi Win64の4環境でプローブを走らせました。受理済みの12 PDF、ソース1,612ページを対象に、48のサンプル/ターゲットペアと、選ばれた意味差のない6,448ページの検証済み出力を得ています。96個すべての操作計測が、報告された秒数と整合する正の生カウンター値を保ちました。そしてそれらの値は意図的に、クロスコンパイラーの速度表へ集約していません。このマトリクスは機能の証明であって、統制された比較ではないからです。読み書きの経路は、すべての埋め込み画像のデコードや、署名の検証や、XFAの実行や、PDF/UAの証明を主張していません。読み書きコストではなく描画スループットを判定する必要があるなら、PDF Library for Delphiにおける並列ページ描画とスレッド安全性の並行性制約のほうが、より良い出発点です

実務的な教訓は短いです。生のカウンターを保ち、順序を交互にし、開始にゲートを置き、ノイズの多いスプレッドは拒み、検証していない出力は決して計測しない。PDF Library for Delphiが「速い」と同じ自信で「計測可能な変化なし」と言えるのは、この規則のおかげです。同じプローブのソースは、DelphiでもFPCでも、Win32でもWin64でも無変更でコンパイルできます。ライブラリー、読み書きAPI、対応コンパイラーは、PDF Library for Delphiの製品ページで確認できます