עיבוד קבצי PDF סטנדרטיים (1MB עד 10MB) ב-Delphi הוא פשוט ומתבצע באמצעות מחלקות זרם (stream classes) רגילות כמו TFileStream או TMemoryStream. עם זאת, כאשר מוטלת עליכם המשימה לעבד קבצי PDF בנפח של גיגה-בתים - כגון סכמות הנדסיות מסיביות ב-CAD, מפות גאו-מרחביות ברזולוציה גבוהה, או ארכיונים משפטיים מצטברים - טכניקות הקצאת זיכרון סטנדרטיות קורסות במהירות.
אם תטענו קובץ PDF בנפח של 2GB לתוך TMemoryStream ביישום Delphi של 32 סיביות, תיתקלו מיד בחריגת EOutOfMemory. אפילו ביישומים של 64 סיביות, פעולה זו גורמת לתקלות דפים חמורות (page faulting) ועוצרת לחלוטין את פעילות השרת. במאמר זה, נחקור כיצד למטב ביצועי קלט/פלט עבור קבצים עצומים באמצעות קבצים ממופים לזיכרון (Memory-Mapped Files).
הבעיה עם זרמים סטנדרטיים
כאשר אתם משתמשים ב-TMemoryStream.LoadFromFile, מערכת ההפעלה קוראת את הקובץ מהדיסק, מקצה זיכרון RAM רציף ומעתיקה אליו את הנתונים. עבור קובץ בנפח 2GB, הדבר מבזבז 2GB של זיכרון RAM פיזי ולוקח זמן משמעותי רק עבור לולאת הקריאה מהדיסק.
אפילו שימוש ב-TFileStream עלול להיות בעייתי אם אתם מדלגים ברחבי הקובץ לעיתים קרובות (לדוגמה, ניתוח טבלת ה-XRef של ה-PDF בסוף הקובץ, ולאחר מכן קפיצה לאובייקטים המפוזרים לאורכו). הקריאות הרצופות ל-Seek ו-Read גורמות לתקורה גבוהה של מעברי ליבה (kernel transitions).
הפתרון: קבצים ממופים לזיכרון (Memory-Mapped Files)
מיפוי לזיכרון (באמצעות פונקציות ה-API של Windows בשם CreateFileMapping ו-MapViewOfFile) מבקש ממערכת ההפעלה למפות את הקובץ ישירות למרחב הכתובות הווירטואלי של היישום. אתם מקבלים מצביע (pointer) לנתונים, ומנהל הזיכרון הווירטואלי של 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 הוא פורמט בעל גישה אקראית. המנתח מתחיל בקריאת ה-trailer בסוף הקובץ, מוצא את טבלת ה-XRef, ואז קופץ באופן אקראי להיסטי בתים לאורך הקובץ כדי לטעון מילונים וזרמים ספציפיים.
עם מיפוי לזיכרון:
- העתקת אפס (Zero-Copy): נתונים לא מועתקים ממרחב הליבה למרחב המשתמש; אתם קוראים ישירות ממטמון הקבצים של מערכת ההפעלה.
- טעינה מיידית: פתיחת PDF בנפח 2GB אורכת אלפיות שנייה, שכן נתונים לא באמת נקראים מהדיסק עד שאתם ניגשים למצביע.
- דפדוף (Paging) מנוהל על ידי מערכת ההפעלה: אם תנתחו רק 50MB מתוך קובץ ה-2GB, מערכת ההפעלה תטען רק את אותם 50MB לתוך ה-RAM הפיזי. צריכת הזיכרון נשארת זעירה.
באמצעות יישום מחלקת זרם מותאמת אישית המגובה בקבצים ממופים לזיכרון, יישום ה-Delphi שלכם יוכל לעבד קבצי PDF בנפח של גיגה-בתים בקלות, תוך שיפור דרמטי בביצועים וביכולת ההרחבה (scalability).
הערה: טיפול ממוטב בזרמי קלט/פלט עבור מסמכים עצומים מובנה ישירות בתוך HotPDF VCL Component.