기술 문서

Delphi PDF 로드/저장 벤치마크의 정직한 노이즈 게이트

PDF Library for Delphi의 정직한 로드/저장 벤치마크는 QueryPerformanceCounter로 LoadFromFile과 SaveToFile의 시간을 재고, raw 틱과 카운터 주파수를 그대로 남기며, 베이스라인과 후보를 A/B, B/A, A/B 순서의 교차 페어로 돌리고, CPU 부하가 25%를 넘는 동안은 시작을 거부하고, range-to-median 편차가 15%를 넘는 결과는 기각하며, 저장된 PDF가 구조, 렌더링, 시맨틱 검증 중 하나라도 통과하지 못하면 그 시간 측정은 전부 버립니다. 이 목록은 관료주의처럼 보입니다. "20% 더 빠르다"는 주장이 재실행에서 증발하는 걸 처음 겪기 전까지는요. 이어지는 절에서는 전용 코퍼스 프로브와 그 비교 러너가 어떻게 여기까지 왔는지 다룹니다. 측정할 수 있을 만큼 기계가 한가하지 않았던 실행과, 그렇다고 하네스가 정확히 판정한 사례를 포함해서입니다

Delphi PDF 벤치마크가 0초를 보고하는 이유는?

측정 대상 연산보다 시계가 거칠게 흘러가면 PDF 로드 벤치마크는 0초를 보고합니다. GetTickCount64가 바로 그런 시계입니다. 밀리초를 반환하지만 Windows에서는 시스템 타이머 인터럽트가 발생할 때만 진행되고, 보통 15.6 ms마다 한 번입니다. PDF Library for Delphi의 대용량 파일 벤치마크 데모 FPC 포트는 TStopwatch를 그 툴체인에서 쓸 수 없어서 이걸 썼는데, 경과 시간을 소수 셋째 자리까지 기록합니다. 작은 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);
  ...

0은 부정확한 숫자보다 나쁩니다. 그 위에 세우는 모든 비교가 0으로 나눠지기 때문입니다. 페어 비교 러너는 최솟값이 0인 arm을 "Zero duration prevents a meaningful ratio"라는 이유로 결론 없음 처리하는데, 이 거부 자체는 맞지만, 그 덕분에 데모 타이밍은 짧은 파일이 사는 바로 그 구간에 측정 공백을 남겼습니다. 같은 데모는 OnProgress 콜백도 설치하므로 타이밍에 콜백 오버헤드가 섞이는데, 깨끗한 로드/저장 측정이 짊어지면 안 되는 비용입니다. 게다가 아카이브된 데모 숫자는 이후에 측정한 어떤 것과도 호환되지 않습니다

QueryPerformanceCounter로 LoadFromFile과 SaveToFile 재기

전용 콘솔 프로브인 Tests/CorpusLoadSave.dpr는 QueryPerformanceCounter로 입력 파일마다 두 연산을 측정합니다. LoadFromFile 더하기 PageCount 읽기, 그리고 LoadFromFile 더하기 PageCount 더하기 SaveToFile입니다. 각 연산은 새 TPDFlib 인스턴스를 쓰고 진행 콜백은 없으며, 인스턴스 생성자와 소멸자는 측정 구간 밖에 둡니다. CSV 기록과 모든 출력 검증도 마찬가지입니다. 카운터는 로드 직전과 마지막 라이브러리 호출 직후에 읽고, LastErrorCode는 두 번째 읽기 뒤에야 가져옵니다

PDFlibPas 코퍼스 프로브의 측정 구간: QueryPerformanceCounter를 LoadFromFile 직전과 마지막 라이브러리 호출 직후에 읽고 PageCount와 SaveToFile는 그 안에 두되, 인스턴스 준비, CSV 기록, 출력 검증, 오류 코드 조회는 모두 측정 구간 밖에 둡니다
raw 틱과 카운터 주파수를 파생 초 값 옆에 함께 기록하므로, 초당 천만 틱에서 8,888틱에 로드되는 CAD 도면은 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');

프로브는 파생 초 값 옆에 raw 틱 수와 카운터 주파수를 기록하고, 소수 아홉 자리와 고정된 . 소수 구분자로 포맷합니다. 누구든 CSV에서 몫을 다시 계산해 검증할 수 있도록 하기 위해서입니다. FPC Win64 빌드에서 CAD 샘플은 초당 10,000,000틱 기준 8,888틱에 로드되어 0.000888800초로 기록됐습니다. 옛 타이머였다면 0으로 반올림됐을 관측값입니다. 프로브는 짧은 값을 잘라 내거나 최소 소요 시간을 대입하거나 추정 타이머 오버헤드를 빼는 일을 의도적으로 하지 않으며, 라이브러리 호출이 실패해도 두 행을 모두 기록하고 0이 아닌 종료 코드를 냅니다. 다만 아홉 자리가 곧 정확도는 아닙니다. 기록 정밀도가 늘어난다고 반복 가능성이 따라오는 건 아니고, 노이즈가 심하거나 0인 관측값은 여전히 하류에서 기각해야 합니다

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 두 빌드 사이의 타이밍 비교는 시작 순서, 시작 조건, 편차가 모두 통제되고 기록될 때만 신뢰할 수 있습니다. 그래서 비교 러너는 최소 세 페어를 A/B, B/A, A/B 순서로 잡습니다. 베이스라인을 늘 먼저 돌리면 후보 쪽에 더 따뜻한 파일 캐시와 다른 열 상태를 조용히 선물하는 셈이고, 순서를 번갈면 그 편향이 한쪽 몫이 아니라 양쪽 arm에 고루 퍼집니다. 각 arm 전에 러너는 입력 파일 전체를 SHA-256으로 해시하는데, 아무것도 바뀌지 않았는지 검증하는 동시에 어느 쪽 arm이든 같은 바이트를 미리 읽게 됩니다. 실행이 끝날 때마다 두 실행 파일과 검증 도구를 다시 해시해서, 재빌드된 바이너리가 시리즈 한가운데 끼어들 수 없게 합니다

러너는 이어서 시스템 전체 CPU 사용률을 초당 한 번 샘플링하고, 샘플이 25% 이하로 떨어졌을 때만 arm을 시작하며, 최대 30초 기다린 뒤 시도를 기각으로 기록합니다. 이 게이트는 시작 조건만 통제할 뿐 그 이상은 아무것도 하지 않습니다. 실행 중에 기계를 격리하지 않으므로 전원 상태, 써멀 스로틀링, 백그라운드 작업, OS 캐싱은 여전히 숫자를 흔들 수 있습니다. 그래서 두 번째 필터는 가장 소박한 의미의 통계입니다. 연산마다 러너는 베이스라인 arm, 후보 arm, 페어 candidate/baseline 비율 분포에 대해 range를 median으로 나눈 값을 계산하고, 셋 중 하나라도 0.15를 넘으면 결과를 finding으로 보고하는 대신 noisy라고 표시합니다

PDFlibPas의 페어 비교 게이트: 세 페어를 A/B, B/A, A/B 순서로 돌리고 각 arm 전에 SHA-256 입력 해시를 하며, 시작 게이트는 CPU가 25% 이하로 떨어지기를 기다리고, LoadFromFile이나 load+SaveToFile arm에서 range-to-median 편차가 0.15를 넘으면 실행을 noisy로 표시합니다
시작 순서를 번갈면 캐시와 열 편향이 양쪽 arm에 고루 퍼지고, same-binary 대조군이 이런 세팅이 증명할 수 있는 것을 보여 줍니다. 1.0 근처의 비율은 반복 가능성을 입증할 뿐, speedup 주장은 절대 아닙니다

same-binary 대조군은 속도가 아니라 반복 가능성을 증명하는 이유는?

same-binary 대조군은 베이스라인과 후보로 동일한 실행 파일을 돌리므로, 1.0 근처의 비율은 측정 세팅이 자기 자신을 재현한다는 것만 증명할 수 있고 구현이 빨라졌다는 걸 보여줄 수는 없습니다. 2026-09-21의 첫 엄격 대조군은 고정밀 FPC Win64 프로브로 admitted 70페이지 tagged 가이드를 상대로 돌았는데, CPU 샘플이 26.5%에서 93.8%까지 분포해서 여섯 번의 시작이 전부 기각됐습니다. 보고서에는 실패 기록만 있고 집계는 없었는데, 기계가 바쁠 때 원하는 결과가 정확히 이것입니다. 같은 날, 바이트 단위로 동일한 입력과 같은 프로브 실행 파일, 바뀌지 않은 임계값으로 재시도하자 여섯 번의 시작이 모두 3초 안에 승인됐습니다. range-to-median 편차는 모두 0.019에서 0.054 사이에 떨어졌고, 비율 중앙값은 LoadFromFile이 1.0084, LoadFromFile + SaveToFile가 0.9872였습니다

그 두 숫자가 확립하는 것은 자격 부여된 관측 창뿐이고 그 이상은 아닙니다. 두 바이너리가 다를 때 안정적인 실행은 descriptive comparison으로 표시되며, 비율이 통계적 유의성이나 speedup 주장이 아니라 관측값이라는 점이 명시적으로 붙습니다. 이 규율이 가장 빛을 발하는 순간은 PDF Library for Delphi 프로파일링과 해시 인덱스로 hot path 교체에서처럼 표적화된 최적화를 검증할 때입니다. 프로파일러는 시간이 어디로 가는지 알려주지만, 변경 사항이 전체 파이프라인과의 접촉을 버텨냈는지는 실제 문서를 상대로 한 통제된 페어 실행만 알려줍니다. 소리 내어 말할 가치가 있는 경계가 하나 더 있습니다. normal-save에는 로드가 포함되고, 러너가 기록하는 peak working set은 프로세스 전체 기준이라 그중 어느 것도 저장만의 메모리라고 귀속할 수 없습니다

세 가지 출력 게이트와 네 컴파일러 매트릭스

생성된 파일이 세 개의 독립 게이트를 통과하지 못하면 PDF Library for Delphi 타이밍은 하나도 인정되지 않습니다. 깨진 PDF를 빨리 쓰는 저장은 더 빠른 저장이 아니기 때문입니다. 벤치마크는 먼저 두 연산이 모두 1을 반환하고 admitted 페이지 수를 보고했는지 확인한 다음, 저장된 PDF 한 개를 이 순서로 검증합니다:

PDFlibPas의 세 가지 출력 게이트: 두 연산이 admitted PageCount와 함께 1을 반환해야 하고, 독립 체커가 저장된 파일을 경고 없이 통과시켜야 하며, 모든 페이지가 소스와 일치하는 페이지별 이미지 SHA-256 집합으로 렌더링되어야 하고, optional content와 측정 구조에서 비시각 시맨틱이 일치해야 합니다
깨진 PDF를 빨리 쓰는 저장은 더 빠른 저장이 아니므로, 구조, 렌더링, 비시각 시맨틱이 모두 출력이 여전히 같은 문서라고 동의할 때만 타이밍이 인정됩니다
  • 구조: 독립 PDF 체커가 저장된 파일을 오류와 경고 없이 통과시켜야 합니다
  • 렌더링: 모든 페이지가 기본 상태로 렌더링되고, 페이지별 이미지 SHA-256 집합이 admitted 소스의 참조 렌더링과 정확히 일치해야 합니다
  • 비시각 시맨틱: 소스에 대한 별도의 시맨틱 비교가 픽셀로는 보이지 않는 선별된 속성을 다루며, 문서화된 범위 안의 optional-content와 측정 구조가 포함됩니다

이 게이트들을 갖춘 상태에서 로컬 코퍼스 전체 매트릭스가 FPC Win32, FPC Win64, Delphi Win32, Delphi Win64로 프로브를 돌렸습니다. admitted PDF 12개, 소스 페이지 1,612장을 상대로 샘플/타깃 페어 48개와 검증된 출력 페이지 6,448장을 만들었고 선별된 시맨틱 차이는 없었습니다. 96개의 연산 측정값 전부가 보고된 초와 일치하는 양의 raw 카운터 값을 유지했으며, 이 값들은 의도적으로 크로스 컴파일러 속도 표로 집계하지 않습니다. 매트릭스는 통제된 비교가 아니라 기능적 증거이기 때문입니다. 로드/저장 경로는 모든 임베디드 이미지를 디코딩하거나 서명을 검증하거나 XFA를 실행하거나 PDF/UA를 인증한다고 주장하지도 않습니다. 로드/저장 비용이 아니라 렌더링 처리량을 판단해야 한다면 PDF Library for Delphi의 병렬 페이지 렌더링과 스레드 안전성의 동시성 제약이 더 나은 출발점입니다

실용적인 결론은 짧습니다. raw 카운터를 남기고, 순서를 번갈고, 시작에 게이트를 두고, noisy 편차는 거부하고, 검증하지 않은 출력의 시간은 절대 재지 마세요. 이 규칙들 덕분에 PDF Library for Delphi는 "faster"만큼이나 "no measurable change"도 자신 있게 말할 수 있고, 같은 프로브 소스가 Delphi와 FPC의 Win32, Win64에서 수정 없이 컴파일됩니다. 라이브러리와 로드/저장 API, 지원 컴파일러는 PDF Library for Delphi 제품 페이지에서 확인할 수 있습니다