Technical Article

Оптимізація продуктивності IO для обробки гігабайтних PDF-файлів

Обробка стандартних PDF-файлів (від 1 МБ до 10 МБ) у Delphi є простою завдяки використанню стандартних класів потоків, таких як TFileStream або TMemoryStream. Однак, коли перед вами стоїть завдання обробки гігабайтних PDF-файлів, наприклад масивних інженерних схем САПР, геопросторових карт з високою роздільною здатністю або накопичених юридичних архівів, стандартні методи розподілу пам'яті швидко перестають працювати.

Якщо ви завантажите PDF-файл розміром 2 ГБ у TMemoryStream у 32-розрядному додатку Delphi, ви негайно отримаєте виняток EOutOfMemory. Навіть у 64-розрядних додатках це призводить до серйозних помилок сторінок пам'яті та зупиняє роботу сервера. У цій статті ми розглянемо, як оптимізувати продуктивність вводу-виводу для масивних файлів за допомогою відображення файлів у пам'ять (Memory-Mapped Files).

Проблема зі стандартними потоками

Коли ви використовуєте TMemoryStream.LoadFromFile, операційна система зчитує файл з диска, виділяє послідовну оперативну пам'ять і копіює в неї дані. Для файлу розміром 2 ГБ це марнує 2 ГБ фізичної оперативної пам'яті і займає значний час лише на цикл читання з диска.

Навіть використання TFileStream може бути проблематичним, якщо ви часто переміщуєтеся по файлу (наприклад, аналізуєте таблицю PDF XRef в кінці файлу, а потім переходите до об'єктів, розкиданих по всьому файлу). Постійні виклики Seek і Read призводять до високих накладних витрат на перехід до ядра.

Рішення: Відображення файлів у пам'ять (Memory-Mapped Files)

Відображення в пам'ять (за допомогою функцій Windows API CreateFileMapping та MapViewOfFile) просить ОС відобразити файл безпосередньо у віртуальний адресний простір програми. Ви отримуєте покажчик на дані, а диспетчер віртуальної пам'яті Windows керує вивантаженням даних у фізичну оперативну пам'ять і з неї суворо по мірі доступу до них.

Ось як ви можете реалізувати високопродуктивний зчитувач файлів, відображених у пам'ять, у Delphi для аналізу 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;

Чому відображення в пам'ять домінує при аналізі PDF

PDF є форматом довільного доступу. Аналізатор починає зі зчитування трейлера в кінці файлу, знаходить таблицю XRef, а потім випадковим чином переходить до байтових зміщень по всьому файлу для завантаження певних словників і потоків.

З відображенням у пам'ять:

  1. Копіювання з нульовими витратами (Zero-Copy): Дані не копіюються з простору ядра в простір користувача; ви читаєте безпосередньо з файлового кешу ОС.
  2. Миттєве завантаження: Відкриття PDF-файлу розміром 2 ГБ займає мілісекунди, оскільки фактично жодні дані не зчитуються з диска, доки ви не розіменуєте покажчик.
  3. Керування сторінками (Paging) за допомогою ОС: Якщо ви аналізуєте лише 50 МБ даних з файлу розміром 2 ГБ, ОС завантажує лише ці 50 МБ у фізичну оперативну пам'ять. Споживання пам'яті залишається крихітним.

Впровадивши власний клас потоку, що підтримується файлами, відображеними в пам'ять, ваша програма Delphi зможе з легкістю перетравлювати гігабайтні PDF-файли, значно покращуючи продуктивність і масштабованість.

Примітка: Оптимізована обробка потоків вводу-виводу для масивних документів вбудована безпосередньо в компонент HotPDF VCL.