Technical Article

最佳化 GB 級 PDF 處理的 IO 效能

在 Delphi 中使用標準串流類別 (如 TFileStreamTMemoryStream) 處理標準 PDF (1MB 到 10MB) 是非常直接的。然而,當您的任務是處理 GB 級的 PDF 時 (例如大型工程 CAD 示意圖、高解析度地理空間地圖或累積的法律檔案),標準的記憶體配置技術很快就會失效。

如果您在 32 位元的 Delphi 應用程式中將 2GB 的 PDF 載入 TMemoryStream 中,您會立刻遇到 EOutOfMemory 例外狀況。即使在 64 位元應用程式中,這樣做也會導致嚴重的分頁錯誤 (page faulting) 並使伺服器停擺。在本文中,我們將探討如何使用記憶體對映檔案 (Memory-Mapped Files) 來最佳化大型檔案的 I/O 效能。

標準串流的問題

當您使用 TMemoryStream.LoadFromFile 時,作業系統會從磁碟讀取檔案、配置連續的 RAM,並將資料複製進去。對於 2GB 的檔案來說,這會浪費 2GB 的實體 RAM,並且僅僅在磁碟讀取迴圈上就會花費大量時間。

即使使用 TFileStream,如果您頻繁地在檔案中跳躍 (例如:解析位於檔案結尾的 PDF XRef 表格,然後跳至散佈在檔案各處的物件),也會產生問題。連續的 SeekRead 呼叫會導致很高的核心轉換 (kernel transition) 負擔。

解決方案:記憶體對映檔案

記憶體對映 (透過 Windows API 函式 CreateFileMappingMapViewOfFile) 要求作業系統將檔案直接對映到應用程式的虛擬位址空間。您會取得一個指向該資料的指標,而 Windows 虛擬記憶體管理員 (Virtual Memory Manager) 會嚴格依照您存取的內容,處理資料進出實體 RAM 的分頁。

以下說明如何在 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 是一種隨機存取的格式。解析器會從讀取檔案結尾的尾部 (trailer) 開始,找到 XRef 表格,然後隨機跳躍到整個檔案中的位元組偏移量,以載入特定的字典與串流。

使用記憶體對映:

  1. 零複製 (Zero-Copy):資料不會從核心空間複製到使用者空間;您可以直接從作業系統檔案快取中讀取。
  2. 即時載入:開啟 2GB 的 PDF 只需要幾毫秒,因為在您取消參照 (dereference) 該指標之前,作業系統並不會實際從磁碟讀取資料。
  3. 作業系統管理分頁:如果您只從 2GB 的檔案中解析 50MB 的資料,作業系統只會將那 50MB 載入實體 RAM。記憶體消耗保持極小。

透過實作由記憶體對映檔案支援的自訂串流類別,您的 Delphi 應用程式可以輕鬆處理 GB 級的 PDF,大幅提升效能與擴充性。

備註:針對大型文件最佳化的 I/O 串流處理,已直接內建於 HotPDF VCL 元件 中。