Artículo técnico

Benchmark honesto de PDF en Delphi: gates contra el ruido

Un benchmark honesto de load/save para PDF Library for Delphi mide LoadFromFile y SaveToFile con QueryPerformanceCounter, conserva los ticks crudos y la frecuencia del contador, corre baseline y candidato en pares alternados A/B, B/A, A/B, se niega a arrancar mientras la carga de CPU esté por encima del 25%, rechaza todo resultado cuyo spread entre rango y mediana supere el 15%, y descarta cada medición cuyo PDF guardado falle la validación estructural, de renderizado o semántica. Esa lista suena a burocracia hasta la primera vez que un claim de "20% más rápido" se evapora al repetir la corrida. Lo que sigue es cómo llegaron hasta ahí el probe dedicado de corpus y su runner de comparación, incluida la corrida en la que la máquina estaba simplemente demasiado ocupada para medir nada y el harness lo dijo correctamente

¿Por qué un benchmark de PDF en Delphi reporta cero segundos?

Un benchmark de carga de PDF reporta cero segundos cuando su reloj hace tick de forma más gruesa que la operación que mide, y GetTickCount64 es exactamente ese tipo de reloj: devuelve milisegundos, pero en Windows solo avanza cuando dispara la interrupción del timer del sistema, comúnmente cada 15.6 ms. El port a FPC del demo de benchmark de archivos enormes de PDF Library for Delphi lo usaba porque TStopwatch no está disponible en esa toolchain, y registra el tiempo transcurrido con tres decimales. Cargar un dibujo CAD chico o un documento tagged corto termina bien dentro de un paso del timer, así que el demo a veces imprimía 0.000 para una carga que claramente hizo trabajo de verdad

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

// dentro del bucle de operaciones
Lib:= TPDFlib.Create;
try
  Lib.OnProgress:= Reporter.Progress;
  Started:= GetTickCount64;
  LoadCode:= Lib.LoadFromFile(InputFile, Password);
  ...

Un cero es peor que un número impreciso, porque toda comparación que usted construya sobre él divide por él. El runner de comparación pareada trata cualquier arm con un mínimo de cero como no concluyente, con el motivo "Zero duration prevents a meaningful ratio", que es el rechazo correcto, pero también significa que las mediciones del demo dejaban un hueco justo donde viven los archivos chicos. El mismo demo además instala un callback OnProgress, así que sus mediciones incluyen la sobrecarga del callback que una medición limpia de load/save no debería cargar, y los números archivados del demo no son intercambiables con nada que se mida después

Medir LoadFromFile y SaveToFile con QueryPerformanceCounter

El probe dedicado de consola, Tests/CorpusLoadSave.dpr, mide dos operaciones por archivo de entrada con QueryPerformanceCounter: LoadFromFile más la lectura de PageCount, y LoadFromFile más PageCount más SaveToFile. Cada operación recibe una instancia TPDFlib fresca y sin callback de progreso, y el constructor y el destructor de la instancia quedan fuera de la región medida, igual que la escritura del CSV y toda la validación de salida. El contador se lee inmediatamente antes de la carga e inmediatamente después de la última llamada a la librería, y LastErrorCode se consulta solo después de la segunda lectura

Región medida del probe de corpus de PDFlibPas: QueryPerformanceCounter se lee inmediatamente antes de LoadFromFile y otra vez después de la última llamada a la librería, con PageCount y SaveToFile adentro, mientras el armado de la instancia, la escritura del CSV, la validación de salida y la consulta del código de error quedan fuera de la región medida
Los ticks crudos y la frecuencia del contador se registran junto a los segundos derivados, así que un dibujo CAD que carga en 8,888 ticks a diez millones de ticks por segundo queda preservado como dato real en lugar de redondearse a cero
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');

El probe escribe el conteo de ticks crudo y la frecuencia del contador junto a los segundos derivados, formateados con nueve decimales y un separador decimal fijo ., así que cualquiera puede recalcular el cociente desde el CSV en vez de confiar a ciegas. En el build FPC Win64 la muestra CAD cargó en 8,888 ticks a 10,000,000 de ticks por segundo, registrado como 0.000888800 segundos, una observación que el viejo timer habría redondeado a cero. El probe deliberadamente no recorta valores cortos, no sustituye una duración mínima ni resta una sobrecarga estimada del timer, y aún así escribe ambas filas con un código de salida distinto de cero cuando una llamada a la librería falla. Eso sí: nueve dígitos no son precisión; más decimales registrados no dicen nada sobre repetibilidad, y las observaciones ruidosas o en cero igual tienen que rechazarse aguas abajo

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

¿Qué hace confiable una comparación de tiempos de load/save de PDF?

Una comparación de tiempos entre dos builds de PDF Library for Delphi solo es confiable cuando el orden de arranque, las condiciones de arranque y el spread se controlan y registran, así que el runner de comparación agenda al menos tres pares en orden A/B, B/A, A/B. Correr siempre el baseline primero le entrega en silencio al candidato un caché de archivos más tibio y un estado térmico distinto; alternar el orden reparte ese sesgo entre los dos arms en lugar de acreditarlo a uno. Antes de cada arm el runner calcula el hash SHA-256 del archivo de entrada completo, con lo cual verifica que nada cambió y prelee los mismos bytes para cualquiera de los dos, y vuelve a hashear los dos ejecutables y las herramientas de validación después de cada corrida para que un binario recompilado no se cuele en medio de una serie

El runner luego muestrea la utilización de CPU de toda la máquina una vez por segundo y arranca el arm solo cuando una muestra cae al 25% o menos, esperando como máximo 30 segundos antes de registrar el intento como rechazado. Ese gate controla la condición de arranque y nada más: no aísla la máquina durante la corrida, y el estado de energía, el thermal throttling, el trabajo en segundo plano y el caching del sistema operativo igual pueden mover los números. Así que el segundo filtro es estadístico en el sentido más llano. Para cada operación el runner calcula rango dividido mediana para el arm baseline, el arm candidato y la distribución de ratios pareados candidato/baseline, y si alguno de los tres supera 0.15 el resultado se marca como ruidoso en lugar de reportarse como hallazgo

Gates de comparación pareada en PDFlibPas: tres pares corren en orden A/B, B/A, A/B con hash SHA-256 de la entrada antes de cada arm, un gate de arranque espera CPU al 25 por ciento o menos, y spreads rango-mediana sobre 0.15 en LoadFromFile o en el arm de carga más SaveToFile marcan la corrida como ruidosa
Alternar el orden de arranque reparte el sesgo de caché y térmico entre los dos arms, y el control con el mismo binario muestra lo que semejante montaje puede probar: ratios cerca de 1.0 establecen repetibilidad, nunca un claim de speedup

¿Por qué un control con el mismo binario prueba repetibilidad y no velocidad?

Un control con el mismo binario corre ejecutables idénticos como baseline y como candidato, así que un ratio cerca de 1.0 solo puede probar que el montaje de medición se repite a sí mismo; jamás puede mostrar que una implementación se puso más rápida. El primer control estricto, el 2026-09-21, usó el probe FPC Win64 de alta resolución contra una guía tagged admitida de 70 páginas, y los seis arranques fueron rechazados porque las muestras de CPU iban del 26.5% al 93.8%. El reporte contenía fallos y ningún agregado, que es exactamente el resultado que usted quiere cuando la máquina está ocupada. Un reintento el mismo día con entradas byte a byte idénticas, el mismo ejecutable del probe y umbrales sin cambios aceptó los seis arranques en menos de 3 segundos; cada spread rango-mediana quedó entre 0.019 y 0.054, y las medianas de los ratios fueron 1.0084 para LoadFromFile y 0.9872 para LoadFromFile + SaveToFile

Ese par de números establece una ventana de observación calificada y nada más. Cuando los dos binarios difieren, una corrida estable se rotula como comparación descriptiva, con la nota explícita de que los ratios son observaciones, no significancia estadística ni un claim de speedup. La disciplina importa más cuando usted está validando optimizaciones puntuales como las descritas en perfilar PDF Library for Delphi y reemplazar hot paths con hash indexes: un profiler le dice a dónde va el tiempo, pero solo una corrida pareada controlada sobre documentos reales le dice si el cambio sobrevivió al contacto con todo el pipeline. Un límite más que vale decir en voz alta: el normal-save incluye la carga, y el pico de working set que el runner registra es de todo el proceso, así que nada de eso es memoria atribuible solo al guardado

Tres gates de salida y una matriz de cuatro compiladores

Ninguna medición de PDF Library for Delphi cuenta salvo que el archivo que produjo pase tres gates independientes, porque un guardado que escribe un PDF roto rápido no es un guardado más rápido. El benchmark primero chequea que ambas operaciones devolvieran 1 y reportaran el conteo de páginas admitido, y después valida el único PDF guardado en este orden:

Tres gates de salida en PDFlibPas: ambas operaciones deben devolver 1 con el PageCount admitido, un checker independiente debe aprobar el archivo guardado sin warnings, cada página debe renderizar a un set de SHA-256 de imágenes por página que matchee la fuente, y la semántica no visual debe coincidir en optional content y estructuras de medición
Un guardado que escribe un PDF roto rápido no es un guardado más rápido, así que una medición solo cuenta cuando estructura, renderizado y semántica no visual coinciden en que la salida sigue siendo el mismo documento
  • Estructura: un checker independiente de PDF debe aprobar el archivo guardado sin errores ni warnings
  • Renderizado: cada página se renderiza en su estado por defecto, y el set de SHA-256 de imágenes por página debe coincidir exactamente con el renderizado de referencia de la fuente admitida
  • Semántica no visual: una comparación semántica separada contra la fuente cubre propiedades seleccionadas que los píxeles no pueden mostrar, incluidas las estructuras de optional content y de medición dentro de su alcance documentado

Con esos gates en su sitio, la matriz completa del corpus local corrió el probe en FPC Win32, FPC Win64, Delphi Win32 y Delphi Win64 sobre 12 PDFs admitidos con 1,612 páginas fuente, dando 48 pares muestra/target y 6,448 páginas de salida validadas sin diferencias semánticas seleccionadas. Las 96 mediciones de operaciones conservaron valores crudos de contador positivos y consistentes con sus segundos reportados, y esos valores deliberadamente no se agregan en una tabla de velocidad entre compiladores, porque la matriz es evidencia funcional y no una comparación controlada. El camino de load/save tampoco pretende decodificar cada imagen embebida, validar firmas, ejecutar XFA ni certificar PDF/UA; si lo que usted necesita juzgar es el throughput de renderizado y no el costo de load/save, las restricciones de concurrencia en renderizado paralelo de páginas y thread safety en PDF Library for Delphi son el mejor punto de partida

La moraleja práctica es corta: conserve los contadores crudos, alterne el orden, ponga gate al arranque, rechace los spreads ruidosos y nunca mida una salida que no haya validado. Esas reglas son las que le permiten a PDF Library for Delphi decir "no hay cambio medible" con la misma confianza que "más rápido", y el mismo fuente del probe compila sin cambios en Delphi y FPC para Win32 y Win64. Puede revisar la librería, su API de load/save y los compiladores soportados en la página de producto de PDF Library for Delphi