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:
- 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.
- 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.
- 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.