Technical Article

기가바이트 규모의 PDF 처리를 위한 IO 성능 최적화

Delphi에서 TFileStream이나 TMemoryStream과 같은 표준 스트림 클래스를 사용하여 표준 PDF(1MB ~ 10MB)를 처리하는 것은 간단합니다. 그러나 대규모 엔지니어링 CAD 회로도, 고해상도 지형 공간 지도 또는 누적된 법률 아카이브와 같은 기가바이트 규모의 PDF를 처리해야 하는 경우 표준 메모리 할당 기술은 금방 무너집니다.

32비트 Delphi 애플리케이션에서 2GB PDF를 TMemoryStream에 로드하면 즉시 EOutOfMemory 예외가 발생합니다. 64비트 애플리케이션의 경우에도 이 작업을 수행하면 심각한 페이지 폴트가 발생하고 서버가 중단됩니다. 이 기사에서는 메모리 매핑된 파일을 사용하여 대용량 파일에 대한 I/O 성능을 최적화하는 방법을 살펴봅니다.

표준 스트림의 문제점

TMemoryStream.LoadFromFile을 사용하면 운영 체제(OS)가 디스크에서 파일을 읽고 순차적인 RAM을 할당한 후 데이터를 복사합니다. 2GB 파일의 경우 2GB의 물리적 RAM이 낭비되며 디스크 읽기 루프에만 상당한 시간이 소요됩니다.

TFileStream을 사용하더라도 파일 내에서 자주 이동하는 경우(예: 파일 끝에 있는 PDF XRef 테이블을 구문 분석한 다음 파일 전체에 흩어져 있는 객체로 이동하는 경우) 문제가 될 수 있습니다. 지속적인 SeekRead 호출은 커널 전환 오버헤드를 높입니다.

해결책: 메모리 매핑된 파일

메모리 매핑(Windows API 함수 CreateFileMappingMapViewOfFile 사용)은 OS에 파일을 애플리케이션의 가상 주소 공간에 직접 매핑하도록 요청합니다. 데이터에 대한 포인터를 얻게 되며, Windows 가상 메모리 관리자는 접근하는 즉시 물리적 RAM에 데이터를 페이징하거나 아웃하는 작업을 처리합니다.

다음은 PDF 파싱을 위해 Delphi에서 고성능 메모리 매핑된 파일 리더를 구현하는 방법입니다.

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는 임의 접근(random-access) 형식입니다. 파서는 파일 끝에 있는 트레일러를 읽는 것으로 시작하여 XRef 테이블을 찾은 다음 특정 딕셔너리와 스트림을 로드하기 위해 파일 전체의 바이트 오프셋으로 임의 이동합니다.

메모리 매핑을 사용하면 다음과 같은 이점이 있습니다.

  1. 제로 카피(Zero-Copy): 데이터가 커널 공간에서 사용자 공간으로 복사되지 않습니다. OS 파일 캐시에서 직접 읽습니다.
  2. 즉각적인 로딩: 포인터를 역참조할 때까지 디스크에서 실제로 데이터를 읽지 않으므로 2GB PDF를 여는 데 밀리초밖에 걸리지 않습니다.
  3. OS 관리 페이징: 2GB 파일에서 50MB의 데이터만 파싱하는 경우 OS는 해당 50MB만 물리적 RAM에 로드합니다. 메모리 소비가 극히 적게 유지됩니다.

메모리 매핑된 파일을 지원하는 사용자 정의 스트림 클래스를 구현함으로써 Delphi 애플리케이션은 기가바이트 규모의 PDF를 쉽게 처리할 수 있으며 성능과 확장성을 크게 향상시킬 수 있습니다.

참고: 대용량 문서에 최적화된 I/O 스트림 처리는 HotPDF VCL Component에 직접 내장되어 있습니다.