Technical Article

ギガバイト規模のPDF処理におけるI/Oパフォーマンスの最適化

Delphiで標準的なPDF(1MBから10MB)を処理する場合、TFileStreamTMemoryStreamのような標準のストリームクラスを使用すれば簡単です。しかし、巨大なエンジニアリングCAD回路図、高解像度の地理空間マップ、蓄積された法務アーカイブなど、ギガバイト規模のPDFの処理を任された場合、標準のメモリ割り当て技術はすぐに破綻します。

32ビットのDelphiアプリケーションで2GBのPDFをTMemoryStreamにロードすると、すぐにEOutOfMemory例外が発生します。64ビットアプリケーションでさえ、そうすると深刻なページフォールトが発生し、サーバーが停止します。この記事では、メモリマップトファイルを使用して巨大なファイルのI/Oパフォーマンスを最適化する方法を探ります。

標準ストリームの問題点

TMemoryStream.LoadFromFileを使用すると、OSはディスクからファイルを読み取り、シーケンシャルなRAMを割り当て、そこにデータをコピーします。2GBのファイルの場合、これにより2GBの物理RAMが無駄になり、ディスクの読み取りループだけでかなりの時間がかかります。

TFileStreamを使用しても、ファイル内を頻繁にジャンプする場合(たとえば、ファイルの最後にあるPDF XRefテーブルを解析してから、ファイル全体に散在するオブジェクトにジャンプする場合)は問題になる可能性があります。継続的なSeekおよびReadの呼び出しは、高いカーネル移行のオーバーヘッドをもたらします。

解決策: メモリマップトファイル

メモリマッピング(Windows API関数のCreateFileMappingおよびMapViewOfFile経由)は、ファイルをアプリケーションの仮想アドレス空間に直接マップするようにOSに要求します。データへのポインタを取得し、Windows仮想メモリマネージャーは、アクセスしたときにのみ物理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はランダムアクセスフォーマットです。パーサーはファイルの最後にあるトレーラーの読み取りから開始し、XRefテーブルを見つけ、次に特定の辞書とストリームをロードするためにファイル全体のバイトオフセットにランダムにジャンプします。

メモリマッピングを使用すると次のようになります。

  1. ゼロコピー: データはカーネル領域からユーザー領域にコピーされません。OSのファイルキャッシュから直接読み取ります。
  2. 瞬時のロード: 2GBのPDFを開くのに数ミリ秒しかかかりません。ポインタを参照解除するまでディスクからデータが実際に読み取られることはないためです。
  3. OSが管理するページング: 2GBのファイルから50MBのデータのみを解析する場合、OSはそれらの50MBのみを物理RAMにロードします。メモリ消費量はごくわずかです。

メモリマップトファイルをバックエンドとするカスタムストリームクラスを実装することで、Delphiアプリケーションはギガバイト規模のPDFを簡単に処理でき、パフォーマンスとスケーラビリティが劇的に向上します。

注: 巨大なドキュメントに対する最適化されたI/Oストリーム処理は、HotPDF VCL Componentに直接組み込まれています。