Technical Article

Otimizar o Desempenho de IO para Processamento de PDF à Escala de Gigabytes

O processamento de PDFs normais (1MB a 10MB) em Delphi é simples utilizando classes de fluxo (stream) padrão como TFileStream ou TMemoryStream. No entanto, quando tem a tarefa de processar PDFs à escala de gigabytes, tais como esquemas CAD de engenharia massivos, mapas geoespaciais de alta resolução ou arquivos legais acumulados, as técnicas padrão de alocação de memória falham rapidamente.

Se carregar um PDF de 2GB para um TMemoryStream numa aplicação Delphi de 32 bits, irá imediatamente deparar-se com uma exceção EOutOfMemory. Mesmo em aplicações de 64 bits, fazê-lo causa falhas de página severas (page faulting) e paralisa o servidor. Neste artigo, exploraremos como otimizar o desempenho de I/O para ficheiros massivos utilizando Ficheiros Mapeados em Memória (Memory-Mapped Files).

O Problema com os Fluxos Padrão

Quando utiliza TMemoryStream.LoadFromFile, o sistema operativo lê o ficheiro do disco, aloca RAM sequencial e copia os dados para ela. Para um ficheiro de 2GB, isto desperdiça 2GB de RAM física e demora um tempo significativo apenas para o ciclo de leitura do disco.

Mesmo utilizar TFileStream pode ser problemático se estiver frequentemente a saltar pelo ficheiro (por exemplo, analisar a tabela XRef do PDF no final do ficheiro e depois saltar para objetos espalhados por todo o ficheiro). As chamadas contínuas de Seek e Read resultam numa elevada sobrecarga de transição do kernel.

A Solução: Ficheiros Mapeados em Memória

O mapeamento de memória (através das funções da API do Windows CreateFileMapping e MapViewOfFile) pede ao sistema operativo para mapear o ficheiro diretamente no espaço de endereçamento virtual da aplicação. Obtém um ponteiro para os dados, e o Gestor de Memória Virtual do Windows trata de colocar e retirar os dados da RAM física estritamente à medida que acede a eles.

Eis como pode implementar um leitor de ficheiros mapeados em memória de alto desempenho em Delphi para análise de PDF:

uses
  Winapi.Windows, System.SysUtils, System.Classes;

type
  TMemoryMappedFileReader = class
  private
    FFileHandle: THandle;
    FMappingHandle: THandle;
    FDataPtr: Pointer;
    FFileSize: Int64;
  public
    constructor Create(const FileName: string);
    destructor Destroy; override;
    property Data: Pointer read FDataPtr;
    property Size: Int64 read FFileSize;
  end;

constructor TMemoryMappedFileReader.Create(const FileName: string);
var
  HighSize, LowSize: DWORD;
begin
  // Open the file with read permissions
  FFileHandle := CreateFile(PChar(FileName), GENERIC_READ, FILE_SHARE_READ, nil,
    OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
  if FFileHandle = INVALID_HANDLE_VALUE then
    RaiseLastOSError;

  // Get the 64-bit file size
  LowSize := GetFileSize(FFileHandle, @HighSize);
  FFileSize := (Int64(HighSize) shl 32) or LowSize;

  // Create the mapping object
  FMappingHandle := CreateFileMapping(FFileHandle, nil, PAGE_READONLY, HighSize, LowSize, nil);
  if FMappingHandle = 0 then
    RaiseLastOSError;

  // Map the file into the virtual address space
  FDataPtr := MapViewOfFile(FMappingHandle, FILE_MAP_READ, 0, 0, 0);
  if FDataPtr = nil then
    RaiseLastOSError;
end;

destructor TMemoryMappedFileReader.Destroy;
begin
  if FDataPtr <> nil then UnmapViewOfFile(FDataPtr);
  if FMappingHandle <> 0 then CloseHandle(FMappingHandle);
  if FFileHandle <> INVALID_HANDLE_VALUE then CloseHandle(FFileHandle);
  inherited;
end;

Por Que Razão o Mapeamento de Memória Domina a Análise de PDF

O PDF é um formato de acesso aleatório. O analisador começa por ler o trailer no final do ficheiro, encontra a tabela XRef e depois salta aleatoriamente para deslocamentos de bytes (byte offsets) em todo o ficheiro para carregar dicionários e fluxos específicos.

Com o mapeamento de memória:

  1. Cópia Zero (Zero-Copy): Os dados não são copiados do espaço do kernel para o espaço do utilizador; lê diretamente da cache de ficheiros do sistema operativo.
  2. Carregamento Instantâneo: Abrir um PDF de 2GB demora milissegundos, uma vez que nenhuns dados são realmente lidos do disco até que desreferencie o ponteiro.
  3. Paginação Gerida pelo SO: Se analisar apenas 50MB de dados de um ficheiro de 2GB, o sistema operativo apenas carrega esses 50MB para a RAM física. O consumo de memória permanece mínimo.

Ao implementar uma classe de fluxo personalizada apoiada por ficheiros mapeados em memória, a sua aplicação Delphi pode processar PDFs à escala de gigabytes com facilidade, melhorando drasticamente o desempenho e a escalabilidade.

Nota: O tratamento otimizado de fluxos de I/O para documentos massivos está integrado diretamente no Componente HotPDF VCL.