Technical Article

Optimizarea performanței IO pentru procesarea PDF-urilor la scară de gigabytes

Procesarea PDF-urilor standard (1MB până la 10MB) în Delphi este simplă utilizând clase de flux standard precum TFileStream sau TMemoryStream. Cu toate acestea, atunci când aveți sarcina de a procesa PDF-uri la scară de gigabytes (de exemplu, scheme CAD inginerești masive, hărți geospațiale de înaltă rezoluție sau arhive legale acumulate), tehnicile standard de alocare a memoriei cedează rapid.

Dacă încărcați un PDF de 2GB într-un TMemoryStream într-o aplicație Delphi pe 32 de biți, veți întâmpina imediat o excepție EOutOfMemory. Chiar și în aplicațiile pe 64 de biți, acest lucru cauzează erori severe de paginare (page faulting) și blochează serverul. În acest articol, vom explora cum să optimizăm performanța I/O pentru fișiere masive folosind fișiere mapate în memorie (Memory-Mapped Files).

Problema cu fluxurile standard

Când utilizați TMemoryStream.LoadFromFile, sistemul de operare citește fișierul de pe disc, alocă memorie RAM secvențială și copiază datele în ea. Pentru un fișier de 2GB, acest lucru irosește 2GB de memorie RAM fizică și necesită timp semnificativ doar pentru bucla de citire de pe disc.

Chiar și utilizarea TFileStream poate fi problematică dacă săriți frecvent prin fișier (de exemplu, parsați tabelul XRef PDF la sfârșitul fișierului, apoi săriți la obiecte împrăștiate pe tot parcursul fișierului). Apelurile continue Seek și Read duc la un overhead ridicat de tranziție a nucleului (kernel transition).

Soluția: Fișiere mapate în memorie

Maparea memoriei (prin funcțiile API Windows CreateFileMapping și MapViewOfFile) solicită sistemului de operare să mapeze fișierul direct în spațiul de adrese virtuale al aplicației. Obțineți un pointer către date, iar Windows Virtual Memory Manager se ocupă de paginarea datelor în și din memoria RAM fizică strict pe măsură ce le accesați.

Iată cum puteți implementa în Delphi un cititor de fișiere mapate în memorie de înaltă performanță pentru parsarea PDF-urilor:

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;

De ce maparea memoriei domină parsarea PDF

PDF este un format cu acces aleatoriu. Parserul începe prin citirea trailer-ului la sfârșitul fișierului, găsește tabelul XRef și apoi sare aleatoriu la decalajele de octeți de pe parcursul fișierului pentru a încărca dicționare și fluxuri specifice.

Cu maparea memoriei:

  1. Copiere zero (Zero-Copy): Datele nu sunt copiate din spațiul kernel în spațiul utilizator; citiți direct din memoria cache de fișiere a sistemului de operare.
  2. Încărcare instantanee: Deschiderea unui PDF de 2GB durează milisecunde, deoarece nicio dată nu este citită efectiv de pe disc până când nu dereferențiați pointerul.
  3. Paginare gestionată de SO: Dacă parsați doar 50MB de date dintr-un fișier de 2GB, sistemul de operare încarcă doar acei 50MB în memoria RAM fizică. Consumul de memorie rămâne infim.

Implementând o clasă de flux personalizată susținută de fișiere mapate în memorie, aplicația dumneavoastră Delphi poate procesa cu ușurință PDF-uri la scară de gigabytes, îmbunătățind dramatic performanța și scalabilitatea.

Notă: Gestionarea optimizată a fluxurilor de I/O pentru documente masive este integrată direct în HotPDF VCL Component.