Um benchmark honesto de load/save da PDF Library for Delphi cronometra o LoadFromFile e o SaveToFile com QueryPerformanceCounter, guarda os ticks em bruto e a frequência do contador, corre a baseline e a candidata em pares alternados A/B, B/A, A/B, recusa-se a arrancar enquanto a carga de CPU estiver acima de 25%, rejeita qualquer resultado cuja dispersão entre amplitude e mediana exceda 15%, e deita fora toda a cronometragem cujo PDF gravado falhe a validação estrutural, de renderização ou semântica. Essa lista parece burocracia até à primeira vez que uma alegação de «20% mais rápido» se evapora numa repetição. O que se segue é como a sonda dedicada de corpus e o seu runner de comparação lá chegaram, incluindo a corrida em que a máquina estava simplesmente demasiado ocupada para medir alguma coisa e o harness disse-o corretamente
Porque é que um benchmark PDF em Delphi reporta zero segundos?
Um benchmark de carga de PDF reporta zero segundos quando o seu relógio tica de forma mais grosseira do que a operação que mede, e o GetTickCount64 é exatamente esse tipo de relógio: devolve milissegundos, mas em Windows só avança quando a interrupção do temporizador de sistema dispara, tipicamente a cada 15,6 ms. O port FPC da demo de benchmark de ficheiros grandes da PDF Library for Delphi usava-o porque o TStopwatch não está disponível nessa toolchain, e regista o tempo decorrido com três casas decimais. Carregar um desenho CAD pequeno ou um documento tagged curto termina bem dentro de um passo do temporizador, por isso a demo às vezes imprimia 0.000 para uma carga que claramente fez trabalho a sério
function ElapsedSeconds(StartTick: QWord): Double;
begin
Result:= (GetTickCount64- StartTick)/ 1000.0;
end;
// dentro do ciclo da operação
Lib:= TPDFlib.Create;
try
Lib.OnProgress:= Reporter.Progress;
Started:= GetTickCount64;
LoadCode:= Lib.LoadFromFile(InputFile, Password);
...
Um zero é pior do que um número impreciso, porque toda a comparação que construa por cima dele divide por ele. O runner de comparação emparelhada trata qualquer braço com um mínimo zero como inconclusivo, com o motivo «Zero duration prevents a meaningful ratio», o que é a recusa certa, mas significa também que as cronometragens da demo deixaram um buraco de medição exatamente onde vivem os ficheiros curtos. A mesma demo instala um callback OnProgress, por isso as suas cronometragens incluem um custo de callback que uma medição limpa de load/save não devia carregar, e os números arquivados da demo não são permutáveis com nada medido mais tarde
Cronometrar LoadFromFile e SaveToFile com QueryPerformanceCounter
A sonda de consola dedicada, Tests/CorpusLoadSave.dpr, mede duas operações por ficheiro de entrada com QueryPerformanceCounter: LoadFromFile mais a leitura do PageCount, e LoadFromFile mais PageCount mais SaveToFile. Cada operação recebe uma instância TPDFlib fresca e nenhum callback de progresso, e o construtor e o destrutor da instância ficam fora da região cronometrada, tal como a escrita do CSV e toda a validação do output. O contador é lido imediatamente antes da carga e imediatamente a seguir à última chamada à biblioteca, e o LastErrorCode só é obtido depois da segunda leitura
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');
A sonda escreve a contagem de ticks em bruto e a frequência do contador ao lado dos segundos derivados, formatados com nove casas decimais e um separador decimal . fixo, para que qualquer um possa recalcular o quociente a partir do CSV em vez de acreditar às cegas. Na build FPC Win64 a amostra CAD carregou em 8.888 ticks a 10.000.000 de ticks por segundo, registados como 0.000888800 segundos — uma observação que o velho temporizador teria arredondado para zero. A sonda deliberadamente não corta valores curtos, não substitui por uma duração mínima, nem subtrai uma estimativa do custo do temporizador, e escreve as duas linhas na mesma com um código de saída diferente de zero quando uma chamada à biblioteca falha. Nove dígitos não são precisão, atenção: mais precisão registada nada diz sobre a repetibilidade, e as observações ruidosas ou nulas continuam a ter de ser rejeitadas a jusante
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));
O que torna credível uma comparação de tempos de load/save de PDF?
Uma comparação de tempos entre duas builds da PDF Library for Delphi só é credível quando a ordem de arranque, as condições de arranque e a dispersão estão todas controladas e registadas, por isso o runner de comparação agenda pelo menos três pares pela ordem A/B, B/A, A/B. Correr sempre a baseline primeiro entrega em segredo à candidata uma cache de ficheiros mais quente e um estado térmico diferente; alternar a ordem espalha esse viés pelos dois braços em vez de o creditar a um só. Antes de cada braço o runner faz o hash do ficheiro de entrada completo com SHA-256, o que ao mesmo tempo verifica que nada mudou e pré-lê os mesmos bytes para qualquer um dos braços, e volta a fazer o hash dos dois executáveis e das ferramentas de validação depois de cada corrida, para que um binário recompilado não se esgueire no meio de uma série
O runner faz então uma amostragem da utilização de CPU de toda a máquina uma vez por segundo e só arranca com o braço quando uma amostra desce a 25% ou abaixo, esperando no máximo 30 segundos antes de registar a tentativa como rejeitada. Esse gate controla a condição de arranque e mais nada: não isola a máquina durante a corrida, e o estado de energia, o thermal throttling, o trabalho em segundo plano e a cache do SO ainda conseguem mexer nos números. Por isso o segundo filtro é estatístico no sentido mais simples. Para cada operação o runner calcula a amplitude dividida pela mediana para o braço da baseline, para o braço da candidata, e para a distribuição dos rácios emparelhados candidata/baseline, e se qualquer dos três exceder 0.15 o resultado é etiquetado como ruidoso em vez de ser reportado como conclusão
Porque é que um controlo com o mesmo binário prova repetibilidade e não velocidade?
Um controlo com o mesmo binário corre executáveis idênticos como baseline e candidata, por isso um rácio perto de 1.0 só pode provar que a configuração de medição se repete a si própria; nunca pode mostrar que uma implementação ficou mais rápida. O primeiro controlo estrito, a 2026-09-21, usou a sonda FPC Win64 de alta resolução contra um guia tagged de 70 páginas admitido, e os seis arranques foram todos rejeitados porque as amostras de CPU andaram entre 26,5% e 93,8%. O relatório continha falhas e nenhum agregado, que é exatamente o desfecho que se quer quando a máquina está ocupada. Uma repetição no mesmo dia com inputs byte a byte idênticos, o mesmo executável de sonda e limiares inalterados aceitou os seis arranques em 3 segundos; todas as dispersões amplitude-mediana ficaram entre 0.019 e 0.054, e as medianas dos rácios foram 1.0084 para LoadFromFile e 0.9872 para LoadFromFile + SaveToFile
Esse par de números estabelece uma janela de observação qualificada e mais nada. Quando os dois binários diferem, uma corrida estável é etiquetada como comparação descritiva, com a nota explícita de que os rácios são observações, e não significância estatística nem uma alegação de aceleração. A disciplina interessa sobretudo quando se estão a validar otimizações dirigidas como as descritas em profiling da PDF Library for Delphi e substituição dos hot paths por hash indexes: um profiler diz-lhe para onde vai o tempo, mas só uma corrida emparelhada controlada sobre documentos reais lhe diz se a mudança sobreviveu ao contacto com o pipeline inteiro. Mais uma fronteira que vale a pena dizer em voz alta — o normal-save inclui a carga, e o pico de working set que o runner regista é de todo o processo, pelo que nada disso é memória atribuível só à gravação
Três gates de output e uma matriz de quatro compiladores
Nenhuma cronometragem da PDF Library for Delphi conta se o ficheiro que produziu não passar em três gates independentes, porque uma gravação que escreve um PDF partido depressa não é uma gravação mais rápida. O benchmark primeiro verifica que as duas operações devolveram 1 e reportaram a contagem de páginas admitida, e depois valida o único PDF gravado por esta ordem:
- Estrutura: um checker PDF independente tem de aprovar o ficheiro gravado sem erros nem avisos
- Renderização: cada página é renderizada no seu estado por defeito, e o conjunto de SHA-256 de imagem por página tem de coincidir exatamente com a renderização de referência da origem admitida
- Semântica não visual: uma comparação semântica separada contra a origem cobre propriedades selecionadas que os pixels não conseguem mostrar, incluindo estruturas de optional-content e de measurement dentro do seu âmbito documentado
Com esses gates no sítio, a matriz completa do corpus local correu a sonda em FPC Win32, FPC Win64, Delphi Win32 e Delphi Win64 sobre 12 PDFs admitidos com 1.612 páginas de origem, dando 48 pares amostra/alvo e 6.448 páginas de output validadas sem diferenças semânticas selecionadas. As 96 medições de operações mantiveram valores positivos do contador em bruto consistentes com os segundos reportados, e esses valores são deliberadamente não agregados numa tabela de velocidade entre compiladores, porque a matriz é evidência funcional e não uma comparação controlada. O caminho de load/save também não pretende descodificar todas as imagens incorporadas, validar assinaturas, executar XFA ou certificar PDF/UA; se precisa de julgar o throughput de renderização e não o custo de load/save, as restrições de concorrência em renderização paralela de páginas e thread safety na PDF Library for Delphi são o melhor ponto de partida
A lição prática é curta: guarde os contadores em bruto, alterne a ordem, filtre o arranque, recuse dispersões ruidosas, e nunca cronometre um output que não validou. São essas regras que deixam a PDF Library for Delphi dizer «nenhuma mudança mensurável» com a mesma confiança que «mais rápida», e o mesmo código-fonte da sonda compila sem alterações em Delphi e FPC para Win32 e Win64. Pode rever a biblioteca, a sua API de load/save e os compiladores suportados na página do produto PDF Library for Delphi