Artigo Técnico

Benchmark honesto de load/save no Delphi: gates de ruído

Um benchmark honesto de load/save da PDF Library for Delphi cronometra LoadFromFile e SaveToFile com QueryPerformanceCounter, guarda os ticks crus e a frequência do contador, roda baseline e candidato em pares alternados A/B, B/A, A/B, se recusa a começar enquanto o load de CPU está acima de 25%, rejeita qualquer resultado cuja dispersão range/mediana passe de 15%, e joga fora toda medição cujo PDF salvo falhe em validação estrutural, de renderização ou semântica. A lista parece burocracia até a primeira vez que uma alegação de "20% faster" evapora num rerun. O que vem a seguir é como a sonda dedicada de corpus e seu runner de comparação chegaram lá, incluindo a rodada em que a máquina estava simplesmente ocupada demais para medir qualquer coisa e o harness disse isso corretamente

Por que um benchmark de PDF em Delphi reporta zero segundos?

Um benchmark de load de PDF reporta zero segundos quando o clock dele tica mais grosseiramente que a operação que ele mede, e o GetTickCount64 é exatamente esse tipo de clock: ele retorna milissegundos, mas no Windows só avança quando a interrupção do timer de sistema dispara, comumente a cada 15,6 ms. A porta FPC da demo de benchmark de arquivos enormes da PDF Library for Delphi o usava porque TStopwatch não está disponível nessa toolchain, e ela registra 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 timer, então a demo às vezes imprimia 0.000 para um load que claramente fez trabalho de verdade

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

// dentro do loop de operações
Lib:= TPDFlib.Create;
try
  Lib.OnProgress:= Reporter.Progress;
  Started:= GetTickCount64;
  LoadCode:= Lib.LoadFromFile(InputFile, Password);
  ...

Um zero é pior que um número impreciso, porque toda comparação que você construir sobre ele divide por ele. O runner de comparação pareada trata qualquer braço com mínimo zero como inconclusivo, com o motivo "Zero duration prevents a meaningful ratio", que é a recusa correta, mas também significa que as medições da demo deixaram uma lacuna exatamente onde os arquivos curtos vivem. A mesma demo também instala um callback OnProgress, então as medições dela incluem overhead de callback que uma medição limpa de load/save não deveria carregar, e números de demo arquivados não são intercambiáveis com nada medido depois

Cronometrando LoadFromFile e SaveToFile com QueryPerformanceCounter

A sonda de console dedicada, Tests/CorpusLoadSave.dpr, mede duas operações por arquivo 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, assim como a escrita do CSV e toda a validação de output. O contador é lido imediatamente antes do load e imediatamente depois da última chamada da biblioteca, e o LastErrorCode é buscado só depois da segunda leitura

Região cronometrada da sonda de corpus do PDFlibPas: o QueryPerformanceCounter é lido imediatamente antes do LoadFromFile e de novo depois da última chamada da biblioteca, com PageCount e SaveToFile dentro, enquanto setup da instância, escrita do CSV, validação de output e a busca do código de erro ficam todos fora da região cronometrada
Os ticks crus e a frequência do contador são registrados ao lado dos segundos derivados, então um desenho CAD que carrega em 8.888 ticks a dez milhões de ticks por segundo fica preservado como dado real em vez de ser arredondado para zero
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 crua 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 consiga recomputar o quociente a partir do CSV em vez de confiar nele. Na build FPC Win64 a amostra CAD carregou em 8.888 ticks a 10.000.000 de ticks por segundo, registrados como 0.000888800 segundos — uma observação que o timer antigo teria arredondado para zero. A sonda deliberadamente não corta valores curtos, não substitui por uma duração mínima, nem subtrai um overhead estimado do timer, e ainda escreve as duas linhas com exit code não zero quando uma chamada da biblioteca falha. Nove dígitos não são exatidão, porém: mais precisão registrada não diz nada sobre repetibilidade, e observações ruidosas ou zero ainda precisam ser rejeitadas mais à frente

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 uma comparação de tempos de load/save de PDF confiável?

Uma comparação de tempos entre duas builds da PDF Library for Delphi só é confiável quando a ordem de início, as condições de início e a dispersão estão todas controladas e registradas, então o runner de comparação agenda pelo menos três pares na ordem A/B, B/A, A/B. Rodar sempre o baseline primeiro entrega discretamente ao candidato um cache de arquivo mais quente e um estado térmico diferente; alternar a ordem espalha esse viés pelos dois braços em vez de creditá-lo a um só. Antes de cada braço o runner calcula o hash SHA-256 do arquivo de entrada completo, o que ao mesmo tempo verifica que nada mudou e pré-lê os mesmos bytes para qualquer um dos braços, e ele calcula de novo o hash dos dois executáveis e das ferramentas de validação após cada rodada, para que um binário rebuildado não se esgueire no meio de uma série

O runner então amostra a utilização de CPU da máquina inteira uma vez por segundo e só começa o braço quando uma amostra cai para 25% ou menos, esperando no máximo 30 segundos antes de registrar a tentativa como rejeitada. Esse gate controla a condição de início e nada mais: ele não isola a máquina durante a rodada, e estado de energia, thermal throttling, trabalho em background e caching do SO ainda podem mover os números. Por isso o segundo filtro é estatístico no sentido mais simples. Para cada operação o runner calcula range dividido pela mediana do braço baseline, do braço candidato e da distribuição das razões pareadas candidato/baseline, e se qualquer um dos três passar de 0,15 o resultado é rotulado de ruidoso em vez de reportado como achado

Gates da comparação pareada no PDFlibPas: três pares rodam na ordem A/B, B/A, A/B com hash SHA-256 do input antes de cada braço, um gate de início espera a CPU em 25 por cento ou menos, e dispersões range/mediana acima de 0,15 no LoadFromFile ou no braço de load mais SaveToFile rotulam a rodada como ruidosa
Alternar a ordem de início espalha o viés de cache e térmico pelos dois braços, e o controle same-binary mostra o que um setup desses pode provar: razões perto de 1,0 estabelecem repetibilidade, nunca uma alegação de speedup

Por que um controle same-binary prova repetibilidade, não velocidade?

Um controle same-binary roda executáveis idênticos como baseline e candidato, então uma razão perto de 1,0 só pode provar que o setup de medição se repete; ele nunca pode mostrar que uma implementação ficou mais rápida. O primeiro controle estrito, em 2026-09-21, usou a sonda FPC Win64 de alta resolução contra um guia tagged admitido de 70 páginas, e os seis inícios foram rejeitados porque as amostras de CPU variaram de 26,5% a 93,8%. O relatório continha falhas e nenhum agregado, que é exatamente o desfecho que você quer quando a máquina está ocupada. Uma nova tentativa no mesmo dia, com inputs byte-idênticos, o mesmo executável da sonda e limiares inalterados, aceitou os seis inícios em menos de 3 segundos; toda dispersão range/mediana ficou entre 0,019 e 0,054, e as medianas das razões foram 1,0084 para LoadFromFile e 0,9872 para LoadFromFile + SaveToFile

Esse par de números estabelece uma janela de observação qualificada e nada mais. Quando os dois binários diferem, uma rodada estável é rotulada como comparação descritiva, com a nota explícita de que as razões são observações, não significância estatística nem alegação de speedup. A disciplina importa mais quando você está validando otimizações direcionadas como as descritas em fazer profiling da PDF Library for Delphi e trocar hot paths por hash indexes: um profiler te diz para onde o tempo vai, mas só uma rodada pareada controlada em documentos reais te diz se a mudança sobreviveu ao contato com o pipeline inteiro. Mais uma fronteira que vale dizer em voz alta — o normal-save inclui o load, e o pico de working set que o runner registra é do processo inteiro, então nada disso é memória atribuível só ao save

Três gates de output e uma matriz de quatro compiladores

Nenhuma medição da PDF Library for Delphi conta se o arquivo que ela produziu não passar em três gates independentes, porque um save que escreve um PDF quebrado rapidamente não é um save mais rápido. O benchmark primeiro confere que as duas operações retornaram 1 e reportaram a contagem de páginas admitida, depois valida o único PDF salvo nesta ordem:

Três gates de output no PDFlibPas: as duas operações precisam retornar 1 com o PageCount admitido, um checker independente precisa passar o arquivo salvo sem warnings, cada página precisa renderizar para um conjunto de SHA-256 de imagem por página que case com a fonte, e a semântica não visual precisa casar em optional content e estruturas de medição
Um save que escreve um PDF quebrado rapidamente não é um save mais rápido, então uma medição só conta quando estrutura, renderização e semântica não visual concordam que o output ainda é o mesmo documento
  • Estrutura: um checker de PDF independente precisa passar o arquivo salvo sem erros nem warnings
  • Renderização: cada página é renderizada no estado default dela, e o conjunto de SHA-256 de imagem por página precisa casar exatamente com a renderização de referência da fonte admitida
  • Semântica não visual: uma comparação semântica separada contra a fonte cobre propriedades selecionadas que pixels não conseguem mostrar, incluindo estruturas de optional-content e de medição dentro do escopo documentado delas

Com esses gates no lugar, a matriz completa do corpus local rodou a sonda em FPC Win32, FPC Win64, Delphi Win32 e Delphi Win64 sobre 12 PDFs admitidos com 1.612 páginas de fonte, dando 48 pares amostra/target e 6.448 páginas de output validadas sem diferenças semânticas selecionadas. As 96 medições de operação mantiveram valores crus de contador positivos e consistentes com os segundos reportados, e esses valores deliberadamente não são agregados numa tabela de velocidade entre compiladores, porque a matriz é evidência funcional, não uma comparação controlada. O caminho de load/save também não alega decodificar toda imagem embutida, validar assinaturas, executar XFA ou certificar PDF/UA; se você precisa julgar throughput de renderização em vez de 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 crus, alterne a ordem, filtre o início, recuse dispersões ruidosas, e nunca cronometre um output que você não validou. Essas regras são o que permitem à PDF Library for Delphi dizer "no measurable change" com a mesma confiança que "faster", e o mesmo fonte da sonda compila sem alteração em Delphi e FPC para Win32 e Win64. Você pode conferir a biblioteca, a API de load/save dela e os compiladores suportados na página de produto da PDF Library for Delphi