在 Delphi 中使用标准流类(如 TFileStream 或 TMemoryStream)处理标准 PDF(1MB 到 10MB)非常简单。但是,当您的任务是处理 GB 级(gigabyte-scale)PDF 时,例如庞大的工程 CAD 原理图、高分辨率地理空间地图或累积的法律档案,标准内存分配技术很快就会崩溃。
如果您在 32 位 Delphi 应用程序中将 2GB 的 PDF 加载到 TMemoryStream 中,您将立即遇到 EOutOfMemory 异常。即使在 64 位应用程序中,这样做也会导致严重的缺页错误(page faulting)并使服务器陷入停顿。在本文中,我们将探讨如何使用内存映射文件(Memory-Mapped Files)来优化大文件的 I/O 性能。
标准流的问题
当您使用 TMemoryStream.LoadFromFile 时,操作系统从磁盘读取文件,分配连续的 RAM,并将数据复制到其中。对于 2GB 的文件,这不仅浪费了 2GB 的物理 RAM,而且仅在磁盘读取循环上就花费了大量时间。
如果您经常在文件中跳转(例如,解析位于文件末尾的 PDF XRef 表,然后跳转到分散在整个文件中的对象),即使使用 TFileStream 也可能存在问题。连续的 Seek 和 Read 调用会导致很高的内核转换开销。
解决方案:内存映射文件
内存映射(通过 Windows API 函数 CreateFileMapping 和 MapViewOfFile)要求操作系统将文件直接映射到应用程序的虚拟地址空间。您将获得一个指向数据的指针,并且 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): 数据不会从内核空间复制到用户空间,而是直接从操作系统文件缓存中读取。
- 即时加载: 打开 2GB 的 PDF 仅需几毫秒,因为在您取消引用(dereference)指针之前,实际上并没有从磁盘读取任何数据。
- 操作系统管理的页面调度: 如果您仅从 2GB 文件中解析 50MB 的数据,操作系统仅将这 50MB 加载到物理 RAM 中。内存消耗保持极小。
通过实现由内存映射文件支持的自定义流类,您的 Delphi 应用程序可以轻松处理 GB 级 PDF,从而显著提高性能和可扩展性。
注意:HotPDF VCL Component 直接内置了针对大型文档的优化 I/O 流处理。