分页读取GB级别超大文件试验

📅 2026/7/4 4:24:31
分页读取GB级别超大文件试验
我们在编程过程中经常会和计算机文件读取操作打交道。随着计算机功能和性能的发展我们需要操作的文件尺寸也是越来越大。在 .NET Framework 中我们一般使用 FileStream 来读取、写入文件流。当文件只有数十 kB 或者数 MB 时一般的文件读取方式如 Read()、ReadAll() 等应用起来游刃有余基本不会感觉到太大的延迟。但当文件越来越大达到数百 MB 甚至数 GB 时这种延迟将越来越明显最终达到不能忍受的程度。通常定义大小在 2GB 以上的文件为超大文件当然这个数值会随着科技的进步越来越大。对于这样规模的文件读取普通方法已经完全不能胜任。这就要求我们使用更高效的方法如内存映射法、分页读取法等。内存映射Memory Mapping内存映射的方法可以使用下面的 Windows API 实现。LPVOID MapViewOfFile(HANDLE hFileMappingObject, DWORD dwDesiredAccess, DWORD dwFileOffsetHigh, DWORD dwFileOffsetLow, DWORD dwNumberOfBytesToMap);虽然使用方便但使用上限制较多比如规定的分配粒度Windows下通常为64KB等。下面贴出内存映射法实例代码供参考但本文不做进一步讨论。内存映射法使用MapViewOfFile分页读取法Paging另外一种高效读取文件的方法就是分页法也叫分段法Segmentation对应的读取单位被称作页Page和段Segment。其基本思想是将整体数据分割至较小的粒度再进行处理以便满足时间、空间和性能方面的要求。分页法的概念使用相当广泛如嵌入式系统中的分块处理Blocks和网络数据的分包传输Packages。在开始研究分页法前先来看看在超大文件处理中最为重要的问题高速随机访问。桌面编程中分页法通常应用于文字处理、阅读等软件有时也应用在大型图片显示等方面。这类软件的一个特点就是数据的局部性无论需要处理的文件有多么大使用者的注意力也可以称为视口ViewPort通常只有非常局部的一点如几页文档和屏幕大小的图片。这就要求了接下来我们要找到一种能够实现高速的随机访问而这种访问效果还不能和文件大小有关否则就失去了高速的意义。事实上以下我们研究的分页法就是利用了「化整为零」的方法通过只读取和显示用户感兴趣的那部分数据达到提升操作速度的目的。参考上图假设计算机上有某文件F其内容为「01234567890123456」引号「」中的内容不含引号下同文件大小为FileLength17字节以PageSize3对F进行分页总页数PageCount6得到页号为0~5的6个页面图中页码页号1。各页面所含数据如下表所示。页号页码内容至头部偏移量 Hex长度0101200 01 0231234503 04 0532367806 07 0833490109 0a 0b3452340c 0d 0e356560f 102可以看到最后一页的长度为2最后一页长度总是小于PageSize。当我们要读取「第n页」的数据即页码n时实际上读取的是页号PageNumbern-1的内容。例如n3时PageNumber2数据为「678」该页数据偏移量范围从0x06至0x08长度为3PageSize。为便于讲述在此约定以下文字中均只涉及页号即PageNumber。参考图2设当PageNumberx时页x的数据范围为[offsetStart, offsetEnd]那么可以用如下的代码进行计算C#2.0。1 offsetStart pageNumber * pageSize; 2 3 if(offsetStart pageSize fileSize) 4 { 5 offsetEnd offsetStart pageSize; 6 } 7 else 8 { 9 offsetEnd fileSize - 1; 10 }我们常用的System.IO.FileStream类有两个重要的方法Seek()和Read()。1 // 将该流的当前位置设置为给定值。 2 public override long Seek ( 3 long offset, 4 SeekOrigin origin 5 ) 6 7 // 从流中读取字节块并将该数据写入给定缓冲区中。 8 public override int Read ( 9 [InAttribute] [OutAttribute] byte[] array, 10 int offset, 11 int count 12 )利用这两个方法我们可以指定每次读取的数据起始位置offsetStart和读取长度offsetEnd - offsetStart这样就可以读到任意指定的页数据。我们可以遍历读取所有页这就相当于普通读取整个文件实际操作中一般不会有需求一次读取上GB的文件。指定PageNumber读取页面数据由于每次读取的数据长度PageSize远远小于文件长度FileSize所以使用分页法能够只读取程序需要的那部分数据最大化提高程序的运行效率。下表是笔者在实验环境下对分页法读取文件的运行效率的测试。CPUIntel Core i3 380M 2.53GHz内存DDR3 2048MB x2硬盘TOSHIBA MK3265GSX (320 GB) 5400 RPM为尽量保证测试质量测试前系统进行了重装、硬盘整理等维护操作。该硬盘性能测试结果如下图所示。下面是为了测试分页法而制作的超大文件读取器界面截图图中读取的是本次试验的用例之一Windows8消费者预览版光盘镜像大小3.40GB。本次测试选择了「大、中、小」3种规格的测试文件作为测试用例分别为#文件名文件内容大小KB1AlishaHead.pngPoser Pro 6贴图11,6112ubuntu-11.10-desktop-i386.isoUbuntu11.10桌面版镜像711,9803Windows8-ConsumerPreview-64bit-ChineseSimplified.isoWindows8消费者预览版64位简体中文版镜像3,567,486通过进行多次读取采集到如下表A所示的文件读取数据结果。表中项目「分页(单页)」表示使用分页读取法但设置页面大小为文件大小即只有1页进行读取。同样的为了解分页读取的性能变化情况使用普通读取方法一次读取采集到另一份数据结果如下表B所示。对用例#1该用例大小仅11MB使用常规单次读取方法仅用不到20ms即将全部内容读取完毕。而当采用分页法随着分页大小越来越小文件被划分为更多的页面尽管随机访问文件内容使得文件操作更加方便但在读取整个文件的时候分页却带来了更多的消耗。例如当分页大小为1KB时文件被分割为11,611个页面。读取整个文件时需要重复调用11,611次FileStream.Read()方法增加了很多消耗如下图所示。图中数据仅为全文读取操作对比从图中可以看到当分页尺寸过分的小1KB时这种过度追求微粒化反而导致了操作性能下降。可以看到即实现了微粒化能够进行随机访问同时仍保有一定量的操作性能分页大小为64KB和1MB是不错的选择。实际上上文介绍的MapViewOfFile函数的推荐分页大小正是64KB。对用例#2该用例大小为695.29MB达到较大的尺寸因此对读取缓存cache需求较高同时也对合适的分页尺寸提出了要求。可以看到和用例#1不同当文件尺寸从11.34MB增加到近700MB时分页尺寸随之相应的扩大是提高操作性能的好方法下图中1MB分页。对用例#3该用例达到3.4GB大小符合我们对超大文件的定义。通过前述2个用例的分析可以推测为获得最佳性能分页大小需继续提高比如从1MB提高到4MB。由于本次试验时间仓促考虑不周未使用「边读取、边丢弃」的测试算法导致分页读取用例#3的数据时数据不断在内存中积累最终引发System.OutOfMemoryException异常使得分页读取完整文件这项测试不能正常完成。这一问题需在下次的试验当中加以解决和避免。引发System.OutOfMemoryException