您的位置:首页 > 其它

(转)使用分页方式读取超大文件的性能试验

2012-05-21 01:04 316 查看
Read extreme large files using paging

by Nobi Conmajia (conmajia@gmail.com)

May 15th, 2012

我们在编程过程中,经常会和计算机文件读取操作打交道。随着计算机功能和性能的发展,我们需要操作的文件尺寸也是越来越大。在.NET Framework中,我们一般使用FileStream来读取、写入文件流。当文件只有数十kB或者数MB时,一般的文件读取方式如Read()、ReadAll()等应用起来游刃有余,基本不会感觉到太大的延迟。但当文件越来越大,达到数百MB甚至数GB时,这种延迟将越来越明显,最终达到不能忍受的程度。

通常定义大小在2GB以上的文件为超大文件(当然,这个数值会随着科技的进步,越来越大)。对于这样规模的文件读取,普通方法已经完全不能胜任。这就要求我们使用更高效的方法,如内存映射法、分页读取法等。

内存映射(Memory Mapping)

内存映射的方法可以使用下面的Windows API实现。

[csharp]
view plaincopyprint?

LPVOID MapViewOfFile(HANDLE hFileMappingObject,
  DWORD dwDesiredAccess,
  DWORD dwFileOffsetHigh,
  DWORD dwFileOffsetLow,
  DWORD dwNumberOfBytesToMap);

[csharp]
view plaincopyprint?

offsetStart = pageNumber * pageSize;

if(offsetStart + pageSize < fileSize)
offsetEnd = offsetStart + pageSize;
else
offsetEnd = fileSize - 1;

offsetStart = pageNumber * pageSize;

if(offsetStart + pageSize < fileSize)
offsetEnd = offsetStart + pageSize;
else
offsetEnd = fileSize - 1;


我们常用的System.IO.FileStream类有两个重要的方法:Seek()和Read()。

[csharp]
view plaincopyprint?

// 将该流的当前位置设置为给定值。
public override long Seek (
long offset,
SeekOrigin origin
)

// 从流中读取字节块并将该数据写入给定缓冲区中。

public override int Read (
[InAttribute] [OutAttribute] byte[] array,
int offset,
int count
)

[csharp]
view plaincopyprint?

指定PageNumber,读取页数据
byte[] getPage(Int64 pageNumber)
{
if (fileStream == null || !fileStream.CanSeek || !fileStream.CanRead)
return null;

if (pageNumber < 0 || pageNumber >= pageCount)
return null;

// absolute offileStreamet of read range
Int64 offsetStart = (Int64)pageNumber * (Int64)pageSize;
Int64 offsetEnd = 0;

if (pageNumber < pageCount - 1)
{
// not last pageNumber
offsetEnd = offsetStart + pageSize - 1;
}
else
{
// last pageNumber

offsetEnd = fileSize - 1;
}

byte[] tmp = new byte[offsetEnd - offsetStart + 1];

fileStream.Seek(offsetStart, SeekOrigin.Begin);
int rd = fileStream.Read(tmp, 0, (Int32)(offsetEnd - offsetStart + 1));

return tmp;
}

指定PageNumber,读取页数据
byte[] getPage(Int64 pageNumber)
{
if (fileStream == null || !fileStream.CanSeek || !fileStream.CanRead)
return null;

if (pageNumber < 0 || pageNumber >= pageCount)
return null;

// absolute offileStreamet of read range
Int64 offsetStart = (Int64)pageNumber * (Int64)pageSize;
Int64 offsetEnd = 0;

if (pageNumber < pageCount - 1)
{
// not last pageNumber
offsetEnd = offsetStart + pageSize - 1;
}
else
{
// last pageNumber
offsetEnd = fileSize - 1;
}

byte[] tmp = new byte[offsetEnd - offsetStart + 1];

fileStream.Seek(offsetStart, SeekOrigin.Begin);
int rd = fileStream.Read(tmp, 0, (Int32)(offsetEnd - offsetStart + 1));

return tmp;
}


由于每次读取的数据长度(PageSize)远远小于文件长度(FileSize),所以使用分页法能够只读取程序需要的那部分数据,最大化提高程序的运行效率。下表是笔者在实验环境下对分页法读取文件的运行效率的测试。

CPU:Intel Core i3 380M @ 2.53GHz

内存:DDR3 2048MB x2

硬盘:TOSHIBA MK3265GSX (320 GB) @ 5400 RPM

为尽量保证测试质量,测试前系统进行了重装、硬盘整理等维护操作。该硬盘性能测试结果如下图所示。



下面是为了测试分页法而制作的超大文件读取器界面截图,图中读取的是本次试验的用例之一Windows8消费者预览版光盘镜像(大小:3.40GB)。



本次测试选择了「大、中、小」3种规格的测试文件作为测试用例,分别为:

#文件名文件内容大小(KB)
1AlishaHead.pngPoser Pro 6贴图11,611
2ubuntu-11.10-desktop-i386.isoUbuntu11.10桌面版镜像711,980
3Windows8-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

尽管如此,通过试验,仍然可以清楚的看到,在常规文件(GB以下级别)操作中,分页法具有高度灵活性,但额外开销大,全文读取速度慢的问题。当操作超大文件(GB以上级别)时,分页法的优势开始显现。极高的数据读取灵活性带来的是和文件大小无关的随机页面访问速度(仅和分页大小有关)。在这个级别上,文件大小往往远远超过常规方法所能读取的最大值(0x7FFFFFFF),因此只有使用分页法,积少成多,才能完成读取完整文件的工作。

分页法使用简单,思路清晰,具有很高的灵活性和与文件长度无关的随机读取能力,最大支持文件大小理论上能够达到8,388,608 TB(Int64)。但同时它也具有额外开销大的特点,因此不适合小文件的操作。

通过扩展该方法,我们可以几乎在所有需要大量、重复、大范围算法处理的程序中加以应用分页法的「化整为零」思想,以减少计算粒度,实现计算的可持续进行。

分页法,以及上文提到的内存映射法,其实均早已出现多年,更是广泛应用于各个行业。笔者之所以仍旧撰写此文,一则锻炼自己的编程能力、语言归纳能力、文字写作能力,二则加深对方法的理解,通过试验得出的现象来深入方法的本质。鉴于笔者才疏学浅,在此妄言,有些词不达意,甚至出现谬误之处,还望各位读者多加批评、指正。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: