Technical Article

Optymalizacja wydajności I/O przy przetwarzaniu gigabajtowych plików PDF

Przetwarzanie standardowych plików PDF (od 1 MB do 10 MB) w Delphi jest proste przy użyciu standardowych klas strumieni, takich jak TFileStream czy TMemoryStream. Jednakże, gdy masz za zadanie przetworzyć gigabajtowe pliki PDF, takie jak ogromne schematy inżynieryjne CAD, mapy przestrzenne o wysokiej rozdzielczości lub skumulowane archiwa prawne, standardowe techniki alokacji pamięci szybko zawodzą.

Jeśli załadujesz 2-gigabajtowy plik PDF do TMemoryStream w 32-bitowej aplikacji Delphi, natychmiast napotkasz wyjątek EOutOfMemory. Nawet w aplikacjach 64-bitowych takie działanie powoduje poważne błędy braku strony (page faulting) i doprowadza serwer do zatrzymania. W tym artykule zbadamy, jak zoptymalizować wydajność wejścia/wyjścia (I/O) dla ogromnych plików przy użyciu plików mapowanych w pamięci (Memory-Mapped Files).

Problem ze standardowymi strumieniami

Kiedy używasz TMemoryStream.LoadFromFile, system operacyjny odczytuje plik z dysku, alokuje sekwencyjną pamięć RAM i kopiuje do niej dane. W przypadku pliku o rozmiarze 2 GB marnuje to 2 GB fizycznej pamięci RAM i zajmuje znaczną ilość czasu na samą pętlę odczytu dysku.

Nawet użycie TFileStream może stanowić problem, jeśli często przemieszczasz się po pliku (np. analizując tabelę XRef PDF na końcu pliku, a następnie skacząc do obiektów rozrzuconych po całym dokumencie). Ciągłe wywołania Seek i Read skutkują dużym narzutem na przełączanie do trybu jądra (kernel transition overhead).

Rozwiązanie: Pliki mapowane w pamięci

Mapowanie pamięci (poprzez funkcje API Windows CreateFileMapping i MapViewOfFile) prosi system operacyjny o zmapowanie pliku bezpośrednio do wirtualnej przestrzeni adresowej aplikacji. Otrzymujesz wskaźnik do danych, a Menedżer Pamięci Wirtualnej systemu Windows zarządza stronicowaniem danych do i z fizycznej pamięci RAM ściśle w momencie uzyskiwania do nich dostępu.

Oto jak możesz zaimplementować wysoce wydajny czytnik plików mapowanych w pamięci w środowisku Delphi do parsowania 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;

Dlaczego mapowanie pamięci dominuje w parsowaniu PDF

PDF jest formatem o dostępie swobodnym. Parser rozpoczyna od odczytania zwiastuna (trailer) na końcu pliku, znajduje tabelę XRef, a następnie skacze losowo do przesunięć bajtowych w całym pliku, aby załadować określone słowniki i strumienie.

Dzięki mapowaniu pamięci:

  1. Brak kopiowania (Zero-Copy): Dane nie są kopiowane z przestrzeni jądra do przestrzeni użytkownika; czytasz bezpośrednio z pamięci podręcznej plików systemu operacyjnego.
  2. Błyskawiczne ładowanie: Otwarcie pliku PDF o rozmiarze 2 GB zajmuje milisekundy, ponieważ żadne dane nie są faktycznie odczytywane z dysku, dopóki nie odwołasz się do wskaźnika.
  3. Stronicowanie zarządzane przez system operacyjny: Jeśli analizujesz tylko 50 MB danych z dwugigabajtowego pliku, system operacyjny ładuje do fizycznej pamięci RAM tylko te 50 MB. Zużycie pamięci pozostaje minimalne.

Implementując niestandardową klasę strumienia opartą na plikach mapowanych w pamięci, Twoja aplikacja w Delphi może z łatwością przetwarzać gigabajtowe pliki PDF, radykalnie poprawiając wydajność i skalowalność.

Uwaga: Zoptymalizowana obsługa strumieni I/O dla ogromnych dokumentów jest wbudowana bezpośrednio w komponent HotPDF VCL Component.