Technical Article

بهینه‌سازی عملکرد IO برای پردازش فایل‌های PDF با حجم گیگابایت

پردازش فایل‌های PDF استاندارد (۱ مگابایت تا ۱۰ مگابایت) در دلفی با استفاده از کلاس‌های استاندارد جریان مانند TFileStream یا TMemoryStream بسیار ساده است. با این حال، زمانی که وظیفه پردازش فایل‌های PDF در مقیاس گیگابایت (مانند نقشه‌های مهندسی CAD، نقشه‌های مکانی با وضوح بالا یا بایگانی‌های حقوقی انباشته شده) را بر عهده دارید، تکنیک‌های استاندارد تخصیص حافظه به سرعت با شکست مواجه می‌شوند.

اگر یک فایل PDF دو گیگابایتی را در یک برنامه ۳۲ بیتی دلفی در TMemoryStream بارگذاری کنید، فوراً با خطای EOutOfMemory مواجه خواهید شد. حتی در برنامه‌های ۶۴ بیتی نیز، انجام این کار باعث خطاهای مکرر صفحه حافظه (page faulting) شده و سرور را به شدت کند می‌کند. در این مقاله، نحوه بهینه‌سازی عملکرد I/O برای فایل‌های حجیم با استفاده از فایل‌های نگاشت شده در حافظه (Memory-Mapped Files) را بررسی خواهیم کرد.

مشکل جریان‌های استاندارد (Standard Streams)

هنگامی که از TMemoryStream.LoadFromFile استفاده می‌کنید، سیستم‌عامل فایل را از دیسک می‌خواند، حافظه RAM متوالی را تخصیص می‌دهد و داده‌ها را در آن کپی می‌کند. برای یک فایل ۲ گیگابایتی، این کار ۲ گیگابایت از حافظه فیزیکی RAM را هدر می‌دهد و زمان قابل توجهی را تنها برای حلقه خواندن از دیسک صرف می‌کند.

حتی استفاده از TFileStream نیز می‌تواند مشکل‌ساز باشد اگر مرتباً در بخش‌های مختلف فایل پرش می‌کنید (مثلاً تجزیه جدول XRef در انتهای فایل، و سپس پرش به اشیاء پراکنده در سراسر فایل). فراخوانی‌های مداوم Seek و Read منجر به سربار بالای انتقال در سطح هسته (kernel transition) می‌شود.

راه‌حل: فایل‌های نگاشت شده در حافظه

نگاشت حافظه (از طریق توابع Windows API به نام‌های CreateFileMapping و MapViewOfFile) از سیستم‌عامل می‌خواهد که فایل را مستقیماً در فضای آدرس مجازی برنامه نگاشت کند. شما یک اشاره‌گر به داده‌ها دریافت می‌کنید و مدیر حافظه مجازی ویندوز، ورود و خروج داده‌ها به حافظه فیزیکی RAM را دقیقاً در زمان دسترسی شما مدیریت می‌کند.

در اینجا نحوه پیاده‌سازی یک خواننده فایل نگاشت شده در حافظه با عملکرد بالا در دلفی برای تجزیه 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 یک فرمت با دسترسی تصادفی (random-access) است. تجزیه‌کننده با خواندن تریلر در انتهای فایل شروع می‌کند، جدول XRef را پیدا می‌کند و سپس به طور تصادفی به افست‌های بایت در سراسر فایل پرش می‌کند تا دیکشنری‌ها و جریان‌های خاص را بارگذاری کند.

با نگاشت حافظه:

  1. کپی صفر (Zero-Copy): داده‌ها از فضای هسته به فضای کاربر کپی نمی‌شوند؛ شما مستقیماً از حافظه پنهان فایل سیستم‌عامل می‌خوانید.
  2. بارگذاری فوری: باز کردن یک فایل PDF دو گیگابایتی چند میلی‌ثانیه طول می‌کشد، زیرا تا زمانی که آدرس اشاره‌گر را فراخوانی نکنید، هیچ داده‌ای در واقع از دیسک خوانده نمی‌شود.
  3. مدیریت صفحه‌بندی توسط سیستم‌عامل: اگر از یک فایل ۲ گیگابایتی تنها ۵۰ مگابایت داده را تجزیه کنید، سیستم‌عامل فقط همان ۵۰ مگابایت را در حافظه فیزیکی RAM بارگذاری می‌کند. مصرف حافظه بسیار کم باقی می‌ماند.

با پیاده‌سازی یک کلاس جریان سفارشی که با فایل‌های نگاشت شده در حافظه پشتیبانی می‌شود، برنامه دلفی شما می‌تواند فایل‌های PDF با حجم گیگابایت را به راحتی پردازش کند و عملکرد و مقیاس‌پذیری را به طور چشمگیری بهبود بخشد.

توجه: مدیریت بهینه جریان I/O برای اسناد حجیم مستقیماً در کامپوننت HotPDF VCL تعبیه شده است.