การประมวลผล PDF มาตรฐาน (1MB ถึง 10MB) ใน Delphi นั้นทำได้อย่างตรงไปตรงมาโดยใช้คลาสสตรีมมาตรฐาน เช่น TFileStream หรือ TMemoryStream อย่างไรก็ตาม เมื่อคุณได้รับมอบหมายให้ประมวลผล PDF ขนาดกิกะไบต์ เช่น แผนผัง CAD วิศวกรรมขนาดใหญ่ แผนที่เชิงพื้นที่ความละเอียดสูง หรือคลังเก็บเอกสารทางกฎหมายที่สะสมไว้ เทคนิคการจัดสรรหน่วยความจำมาตรฐานจะล้มเหลวอย่างรวดเร็ว
หากคุณโหลด PDF ขนาด 2GB ลงใน TMemoryStream ในแอปพลิเคชัน Delphi แบบ 32 บิต คุณจะพบกับข้อยกเว้น EOutOfMemory ทันที แม้แต่ในแอปพลิเคชันแบบ 64 บิต การทำเช่นนั้นก็ทำให้เกิด page faulting อย่างรุนแรงและทำให้เซิร์ฟเวอร์หยุดชะงัก ในบทความนี้ เราจะสำรวจวิธีการเพิ่มประสิทธิภาพ I/O สำหรับไฟล์ขนาดใหญ่โดยใช้ Memory-Mapped Files (ไฟล์ที่แมปกับหน่วยความจำ)
ปัญหาของ Standard Streams (สตรีมมาตรฐาน)
เมื่อคุณใช้ TMemoryStream.LoadFromFile ระบบปฏิบัติการจะอ่านไฟล์จากดิสก์ จัดสรร RAM ตามลำดับ และคัดลอกข้อมูลลงไป สำหรับไฟล์ขนาด 2GB สิ่งนี้จะสิ้นเปลือง RAM จริงไป 2GB และใช้เวลาอย่างมากเพียงเพื่อวงรอบการอ่านดิสก์
แม้แต่การใช้ TFileStream ก็อาจเป็นปัญหาได้หากคุณต้องกระโดดข้ามไปมาภายในไฟล์บ่อยๆ (เช่น การแยกวิเคราะห์ตาราง PDF XRef ที่ส่วนท้ายของไฟล์ จากนั้นกระโดดไปยังออบเจ็กต์ที่กระจัดกระจายอยู่ทั่วทั้งไฟล์) การเรียก Seek และ Read อย่างต่อเนื่องส่งผลให้เกิดโอเวอร์เฮดของการเปลี่ยนผ่านเคอร์เนลที่สูง
วิธีแก้ปัญหา: Memory-Mapped Files
การแมปหน่วยความจำ (ผ่านฟังก์ชัน Windows API CreateFileMapping และ MapViewOfFile) เป็นการขอให้ระบบปฏิบัติการแมปไฟล์ลงในพื้นที่ที่อยู่เสมือนของแอปพลิเคชันโดยตรง คุณจะได้รับพอยน์เตอร์ไปยังข้อมูล และ Virtual Memory Manager ของ 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 ใช้เวลาเพียงเสี้ยววินาที เนื่องจากจะไม่มีการอ่านข้อมูลจากดิสก์จริงๆ จนกว่าคุณจะดีเรเฟอเรนซ์พอยน์เตอร์
- OS จัดการการทำเพจจิ้ง (OS Managed Paging): หากคุณแยกวิเคราะห์ข้อมูลเพียง 50MB จากไฟล์ขนาด 2GB ระบบปฏิบัติการจะโหลดแค่ 50MB นั้นลงใน RAM จริง การใช้หน่วยความจำยังคงมีขนาดเล็กมาก
ด้วยการนำคลาสสตรีมแบบกำหนดเองที่สนับสนุนโดยไฟล์ที่แมปกับหน่วยความจำมาใช้ แอปพลิเคชัน Delphi ของคุณจะสามารถประมวลผล PDF ขนาดกิกะไบต์ได้อย่างง่ายดาย ช่วยปรับปรุงประสิทธิภาพและความสามารถในการขยายขนาดได้อย่างมาก
หมายเหตุ: การจัดการสตรีม I/O ที่ปรับให้เหมาะสมสำหรับเอกสารขนาดใหญ่ถูกสร้างขึ้นโดยตรงใน HotPDF VCL Component