Честный load/save-бенчмарк PDF Library for Delphi замеряет LoadFromFile и SaveToFile через QueryPerformanceCounter, хранит сырые тики и частоту счётчика, гоняет базовую и кандидатскую сборки в чередующихся парах A/B, B/A, A/B, отказывается стартовать, пока загрузка CPU сидит выше 25%, бракует любой результат, у которого разброс «размах к медиане» превышает 15%, и выбрасывает каждый замер, чей сохранённый PDF не прошёл структурную, рендерную или семантическую валидацию. Список читается как бюрократия, пока «быстрее на 20%» впервые не испарится при повторном прогоне. Дальше — как корпусный пробник и его раннер сравнения к этому пришли, включая прогон, когда машина была просто слишком занята, чтобы что-то мерить, и харнесс правильно об этом сказал
Почему Delphi-бенчмарк PDF сообщает ноль секунд?
Бенчмарк загрузки PDF сообщает ноль секунд, когда его часы тикают грубее, чем измеряемая операция, и GetTickCount64 — часы ровно такого сорта: они возвращают миллисекунды, но на Windows продвигаются только когда срабатывает прерывание системного таймера, обычно раз в 15,6 мс. FPC-порт демо-бенчмарка огромных файлов в PDF Library for Delphi использовал его, потому что TStopwatch в этом тулчейне недоступен, и записывал прошедшее время с тремя знаками после запятой. Загрузка небольшого CAD-чертежа или короткого тегированного документа укладывается глубоко внутрь одного шага таймера, так что демо иногда печатало 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, так что его замеры несут накладные расходы колбэка, которые чистое измерение load/save нести не должно, и архивированные демо-цифры не взаимозаменяемы ни с чем, что измерено позже
Замеряем LoadFromFile и SaveToFile через QueryPerformanceCounter
Выделенный консольный пробник Tests/CorpusLoadSave.dpr измеряет для каждого входного файла две операции через QueryPerformanceCounter: LoadFromFile плюс чтение PageCount и LoadFromFile плюс PageCount плюс SaveToFile. Каждая операция получает свежий экземпляр TPDFlib и никакого колбэка прогресса, а конструктор и деструктор экземпляра сидят вне замеряемой области, как и запись 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');
Пробник пишет сырой счёт тиков и частоту счётчика рядом с выведенными секундами, форматируя с девятью знаками после запятой и фиксированным разделителем ., так что любой может пересчитать частное из CSV, а не верить ему на слово. На сборке FPC Win64 CAD-образец загрузился за 8 888 тиков при 10 000 000 тиков в секунду, записано как 0.000888800 секунды — наблюдение, которое старый таймер округлил бы в ноль. Пробник сознательно не обрезает короткие значения, не подставляет минимальную длительность и не вычитает оценённые накладные расходы таймера, а при сбое вызова библиотеки всё равно пишет обе строки с ненулевым кодом выхода. Только девять цифр — ещё не точность: больше записанной точности ничего не говорит о повторяемости, и зашумлённые или нулевые наблюдения всё равно приходится отбраковывать дальше по конвейеру
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 заслуживает доверия, только когда порядок старта, условия старта и разброс проконтролированы и записаны, поэтому раннер сравнения планирует минимум три пары в порядке A/B, B/A, A/B. Всегда первым гонять базовую сборку — значит молча даровать кандидату более тёплый файловый кэш и другое тепловое состояние; чередование порядка размазывает это смещение по обеим рукам вместо того, чтобы записать его одной. Перед каждой рукой раннер считает SHA-256 полного входного файла — это и проверяет, что ничего не изменилось, и заранее читает одни и те же байты для любой руки, — а после каждого прогона снова хеширует оба исполняемых файла и инструменты валидации, чтобы пересобранный бинарник не проскочил в середину серии
Затем раннер раз в секунду снимает общесистемную загрузку CPU и стартует руку только когда сэмпл опускается до 25% или ниже, выжидая максимум 30 секунд, прежде чем записать попытку как отклонённую. Этот фильтр контролирует условие старта и больше ничего: изолировать машину на время прогона он не может, и состояние питания, троттлинг, фоновая работа и кэширование ОС всё ещё способны двигать цифры. Поэтому второй фильтр — статистический в самом прямом смысле. Для каждой операции раннер считает «размах делить на медиану» для базовой руки, руки-кандидата и распределения парных отношений кандидат/база, и если хоть одна из трёх величин превышает 0.15, результат помечается как зашумлённый вместо того, чтобы попадать в отчёт как находка
Почему контроль с одинаковым бинарником доказывает повторяемость, а не скорость?
Контроль с одинаковым бинарником гоняет идентичные исполняемые файлы в роли базы и кандидата, поэтому отношение около 1.0 может доказать только то, что измерительная установка повторяет саму себя; показать, что реализация стала быстрее, оно не способно никогда. Первый строгий контроль 2026-09-21 брал высокоточный пробник FPC Win64 против принятого тегированного руководства на 70 страниц, и все шесть стартов были отклонены, потому что сэмплы CPU гуляли от 26.5% до 93.8%. Отчёт содержал отказы и ни одного агрегата — ровно тот исход, который вам нужен, когда машина занята. Повтор в тот же день с побайтово идентичными входами, тем же исполняемым файлом пробника и неизменными порогами принял все шесть стартов в пределах 3 секунд; каждый разброс «размах к медиане» лёг между 0.019 и 0.054, а медианы отношений составили 1.0084 для LoadFromFile и 0.9872 для LoadFromFile + SaveToFile
Эта пара чисел устанавливает квалифицированное окно наблюдения и не больше ничего. Когда бинарники различаются, стабильный прогон помечается как описательное сравнение с прямой оговоркой: отношения — это наблюдения, а не статистическая значимость и не претензия на ускорение. Дисциплина важнее всего, когда вы валидируете точечные оптимизации вроде описанных в статье о профилировании PDF Library for Delphi и замене горячих путей хеш-индексами: профайлер говорит, куда уходит время, но только контролируемый парный прогон на реальных документах говорит, пережило ли изменение встречу со всем конвейером. Ещё одна граница, которую стоит проговорить вслух, — normal-save включает загрузку, а пиковый рабочий набор, который записывает раннер, измеряется на весь процесс, так что к одной лишь записи из него относится ноль
Три фильтра вывода и матрица на четыре компилятора
Ни один замер PDF Library for Delphi не считается, пока созданный им файл не пройдёт три независимых фильтра, потому что запись, которая быстро пишет битый PDF, — не более быстрая запись. Бенчмарк сперва проверяет, что обе операции вернули 1 и сообщили принятый счёт страниц, а затем валидирует единственный сохранённый PDF в таком порядке:
- Структура: независимый PDF-чекер должен пропустить сохранённый файл без ошибок и предупреждений
- Рендеринг: каждая страница рендерится в состоянии по умолчанию, а набор постраничных SHA-256 изображений должен точно совпасть с эталонным рендером принятого источника
- Невизуальная семантика: отдельное семантическое сравнение с источником покрывает выбранные свойства, которых пиксели показать не могут, включая структуры optional content и измерений в рамках их задокументированной области
С этими фильтрами полная локальная корпусная матрица прогнала пробник на FPC Win32, FPC Win64, Delphi Win32 и Delphi Win64 по 12 принятым PDF с 1 612 исходными страницами, получив 48 пар образец/цель и 6 448 провалидированных страниц вывода без выбранных семантических различий. Все 96 замеров операций сохранили положительные сырые значения счётчика, согласованные с их сообщёнными секундами, и эти значения сознательно не агрегируются в таблицу скоростей между компиляторами, потому что матрица — функциональное доказательство, а не контролируемое сравнение. Путь load/save также не заявляет, что декодирует каждое встроенное изображение, валидирует подписи, исполняет XFA или сертифицирует PDF/UA; если судить нужно о пропускной способности рендеринга, а не о цене load/save, лучше начать с ограничений конкурентности из статьи о параллельном рендеринге страниц и потокобезопасности в PDF Library for Delphi
Практический вывод короток: храните сырые счётчики, чередуйте порядок, фильтруйте старт, отклоняйте зашумлённые разбросы и никогда не замеряйте вывод, который не провалидировали. Эти правила позволяют PDF Library for Delphi говорить «измеримых изменений нет» так же уверенно, как «быстрее», а исходник пробника компилируется без изменений на Delphi и FPC для Win32 и Win64. Библиотеку, её load/save API и поддерживаемые компиляторы можно посмотреть на странице продукта PDF Library for Delphi