在 Delphi 中使用標準串流類別 (如 TFileStream 或 TMemoryStream) 處理標準 PDF (1MB 到 10MB) 是非常直接的。然而,當您的任務是處理 GB 級的 PDF 時 (例如大型工程 CAD 示意圖、高解析度地理空間地圖或累積的法律檔案),標準的記憶體配置技術很快就會失效。
如果您在 32 位元的 Delphi 應用程式中將 2GB 的 PDF 載入 TMemoryStream 中,您會立刻遇到 EOutOfMemory 例外狀況。即使在 64 位元應用程式中,這樣做也會導致嚴重的分頁錯誤 (page faulting) 並使伺服器停擺。在本文中,我們將探討如何使用記憶體對映檔案 (Memory-Mapped Files) 來最佳化大型檔案的 I/O 效能。
標準串流的問題
當您使用 TMemoryStream.LoadFromFile 時,作業系統會從磁碟讀取檔案、配置連續的 RAM,並將資料複製進去。對於 2GB 的檔案來說,這會浪費 2GB 的實體 RAM,並且僅僅在磁碟讀取迴圈上就會花費大量時間。
即使使用 TFileStream,如果您頻繁地在檔案中跳躍 (例如:解析位於檔案結尾的 PDF XRef 表格,然後跳至散佈在檔案各處的物件),也會產生問題。連續的 Seek 和 Read 呼叫會導致很高的核心轉換 (kernel transition) 負擔。
解決方案:記憶體對映檔案
記憶體對映 (透過 Windows API 函式 CreateFileMapping 和 MapViewOfFile) 要求作業系統將檔案直接對映到應用程式的虛擬位址空間。您會取得一個指向該資料的指標,而 Windows 虛擬記憶體管理員 (Virtual Memory Manager) 會嚴格依照您存取的內容,處理資料進出實體 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):資料不會從核心空間複製到使用者空間;您可以直接從作業系統檔案快取中讀取。
- 即時載入:開啟 2GB 的 PDF 只需要幾毫秒,因為在您取消參照 (dereference) 該指標之前,作業系統並不會實際從磁碟讀取資料。
- 作業系統管理分頁:如果您只從 2GB 的檔案中解析 50MB 的資料,作業系統只會將那 50MB 載入實體 RAM。記憶體消耗保持極小。
透過實作由記憶體對映檔案支援的自訂串流類別,您的 Delphi 應用程式可以輕鬆處理 GB 級的 PDF,大幅提升效能與擴充性。
備註:針對大型文件最佳化的 I/O 串流處理,已直接內建於 HotPDF VCL 元件 中。