C++从文件的几个部分读取太慢

tmi*_*hty 5 c++ optimization

我需要从一个大文件的几个位置读取字节数组.我已经对文件进行了优化,以便尽可能少地阅读部分,并且这些部分尽可能紧密地结合在一起.

我有20个这样的电话:

m_content.resize(iByteCount);

fseek(iReadFile,iStartPos ,SEEK_SET);
size_t readElements = fread(&m_content[0], sizeof(unsigned char), iByteCount, iReadFile); 
Run Code Online (Sandbox Code Playgroud)

iByteCount平均约为5000.

在使用fread之前,我使用了内存映射文件,但结果大致相同.

第一次呼叫时,我的呼叫仍然太慢(大约200毫秒).当我用相同的字节部分重复相同的调用来读取时,它非常快(大约1毫秒),但这对我没有帮助.

文件很大(大约200 MB).在此调用之后,我必须从文件的不同部分读取双值,但我无法避免这种情况.

我不想将其拆分为2个文件.我已经看到了其他人使用的"巨大文件方法",他们以某种方式克服了这个问题.

如果我使用内存映射,第一次读取调用总是很慢.如果我再重复阅读本节,它会快速闪电.当我从另一个部分读取时,它第一次很慢,但第二次快速闪电.

我不知道为什么会这样.

有没有人对我有任何想法?谢谢.

Dam*_*mon 6

磁盘驱动器有两个(实际上是三个)限制其速度的因素:访问时间,顺序带宽和总线延迟/带宽.

你最感觉的是访问时间.访问时间通常在毫秒球场.在典型的硬盘上,必须进行搜索需要超过5(通常超过10)毫秒.请注意,打印在磁盘驱动器上的数字是"平均"时间,而不是最差时间(在某些情况下,它似乎更接近"最佳"而不是"平均").

即使对于慢速磁盘,顺序读取带宽通常高达60-80 MiB/s,对于更快的磁盘(或固态> 400MiB),顺序读取带宽通常为120-150 MiB/s.总线带宽和延迟是您通常不关心的事情,因为总线速度通常超过驱动器速度(除非您使用SATA-2上的现代固态磁盘,或SATA-1上的15k硬盘或USB上的任何磁盘) ).

另请注意,您无法更改驱动器的带宽,也无法更改总线带宽.你也不能改变寻找时间.但是,您可以更改搜索次数.

在实践中,这意味着你必须尽可能地避免寻求.如果这意味着读取您不需要的数据,请不要害怕这样做.这是快读100比昆明植物研究所读5昆明植物研究所,寻求未来90千字节,并读取另一个5昆明植物研究所.

如果可以,一次阅读整个文件,只使用您感兴趣的部分.200 MiB不应该是现代计算机上的一大障碍.fread然而,将200 MiB读入分配的缓冲区可能是禁止的(这取决于您的目标体系结构,以及您的程序正在做什么).但不要担心,您已经拥有了解决问题的最佳解决方案:内存映射.
虽然记忆映射不是一个"神奇的加速器",但它仍然尽可能接近"神奇".

内存映射的一大优点是可以直接从缓冲区缓存中读取.这意味着操作系统将预取页面,你甚至可以要求它更积极地预取,所以你的所有读取都将是"即时的".此外,存储在缓冲区高速缓存中的内容在某种意义上是"免费的".
不幸的是,内存映射并不总是很容易(特别是因为操作系统通常提供的文档和提示标志具有欺骗性或适得其反的效果).

虽然您无法保证已经读取的内容一旦保留在缓冲区中,但实际情况是任何"合理"大小的情况.当然,操作系统不能也不会将数TB的数据保存在RAM中,但是大约200 MiB的内容将非常可靠地保留在"普通"现代计算机上的缓冲区中.从缓冲区读取或多或少在零时间内工作.
因此,您的目标是让操作系统尽可能按顺序将文件读​​入其缓冲区.除非机器耗尽物理内存,因此它被迫丢弃缓冲区页面,这将是快速的(如果发生这种情况,其他所有解决方案都会同样慢).

Linux有readahead系统调用,可以预取数据.不幸的是,它会一直阻塞,直到获取数据,这不是您可能想要的(因此您必须使用额外的线程).madvise(MADV_WILLNEED)是一个不太可靠,但可能更好的选择.posix_fadvise也可以工作,但请注意Linux将readahead限制为默认预读大小的两倍(即256kiB).
不要让自己被文档愚弄,因为文档具有欺骗性.看起来这MADV_RANDOM是一个更好的选择,因为您的访问是"随机的".对操作系统诚实地说你正在做什么是不合理的,不是吗?通常是的,但不是这里.这只是关闭预取,这与你真正想要的完全相反.我不知道这背后的理由,也许是一些不明智的尝试来收敛记忆 - 无论如何它都会对你的表现产生不利影响.

Windows(因为Windows 8,仅适用于桌面)具有PrefetchVirtualMemory,它可以完全满足人们的需求,但不幸的是它只能在最新版本上使用.在旧版本中,只有...没有.

在映射中填充页面的一种非常简单,高效且可移植的方法是启动一个错误每个页面的工作线程.这听起来很可怕,但它的工作非常好,并且与操作系统无关.
类似的东西volatile int x = 0; for(int i = 0; i < len; i += 4096) x += map[i];就足够了.我正在使用这样的代码在访问它们之前对故障前页面进行预故障,它的工作速度与任何其他填充缓冲区的方法无关,并且使用的CPU很少.


tro*_*foe 4

(根据OP的要求移至答案)

您无法更快地读取文件(没有神奇的标志说“读取更快”)。您的硬件有问题,或者 200 毫秒是应该花费的时间