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

C++ Reading from several sections of a file is too slow

本文关键字:几个 读取 文件 C++      更新时间:2023-10-16

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

我有20个这样的电话:

m_content.resize(iByteCount);
fseek(iReadFile,iStartPos ,SEEK_SET);
size_t readElements = fread(&m_content[0], sizeof(unsigned char), iByteCount, iReadFile); 

iByteCount平均约为5000。

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

当我第一次被呼叫时,我的呼叫仍然太慢(大约200毫秒)。当我用相同的字节段重复相同的调用时,它非常快(大约1毫秒),但这对我没有真正的帮助。

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

我不想把它分成两个文件。我也看到过其他人使用的"大文件方法",他们以某种方式克服了这个问题。

如果我使用记忆映射,第一次阅读总是很慢。如果我重复阅读这一部分,它会很快变亮。当我从另一个章节阅读时,第一次读起来很慢,但第二次读起来又很快。

我不知道为什么会这样。

有人对我还有什么想法吗?非常感谢。

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

你感觉最深的是访问时间。访问时间通常在毫秒左右。在一个典型的硬盘上,必须进行查找需要5毫秒以上(通常超过10毫秒)。请注意,打印在磁盘驱动器上的数字是"平均"时间,而不是最坏的时间(在某些情况下,它似乎更接近"最佳"而不是"平均")。

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

还要注意,您不能更改驱动器的带宽,也不能更改总线带宽。您也无法更改搜索时间。但是,可以更改搜索次数

在实践中,这意味着您必须尽可能地避免查找。如果这意味着要读取不需要的数据,请不要害怕这样做。读取100 kiB比读取5 kiB、提前查找90 KB并再读取5 kiB快得多。

如果可以的话,一次性阅读整个文件,只使用你感兴趣的部分。200 MiB在现代计算机上应该不会成为一个大障碍。然而,将具有fread的200 MiB读取到分配的缓冲区中可能是禁止的(这取决于您的目标体系结构以及您的程序正在执行的其他操作)。但别担心,您已经有了解决这个问题的最佳方案:内存映射
虽然内存映射不是一个"神奇的加速器",但它仍然尽可能接近"神奇"。

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

虽然你不能保证已经读取的内容一次就留在缓冲区中,但在实践中,任何"合理"大小的东西都是这样。当然,操作系统不能也不会将1TB的数据保存在RAM中,但大约200兆字节的数据将非常可靠地保存在"普通"现代计算机的缓冲区中。从缓冲区读取或多或少可以在零时间内完成
因此,您的目标是让操作系统尽可能按顺序将文件读取到缓冲区中。除非机器耗尽物理内存,因此被迫丢弃缓冲区页面,否则这将是闪电般的快(如果发生这种情况,其他所有解决方案都将同样缓慢)。

Linux有readahead系统调用,它允许您预取数据。不幸的是,它会阻塞,直到数据被提取出来,这可能不是您想要的(因此您必须为此使用额外的线程)。madvise(MADV_WILLNEED)是一种不太可靠但可能更好的替代方案。posix_fadvise也可以工作,但请注意,Linux将预读限制为默认预读大小的两倍(即256kiB)
不要被医生愚弄,因为医生是骗人的。MADV_RANDOM似乎是一个更好的选择,因为您的访问是"随机的"。对操作系统诚实地说你在做什么是有道理的,不是吗?通常是的,但不是在这里这只需关闭预取,这与您真正想要的完全相反。我不知道这背后的理由,也许是一些不明智的恢复记忆的尝试——无论如何,这对你的表现都是有害的。

Windows(自Windows 8以来,仅适用于台式机)具有PrefetchVirtualMemory,这正是人们想要的,但不幸的是,它只在最新版本上可用。在旧版本中,只有。。。没有什么

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

(根据OP的请求移动到一个答案)

你无法更快地从文件中读取(没有神奇的标志来表示"读取速度更快")。硬件或200mS有问题,需要多长时间

1)第一次读取和后续读取之间的访问速度差异是完全可以理解的:第一次调用实际上是从磁盘读取文件,这需要时间。但是,您的内核(没有提及磁盘控制器)会缓冲访问的数据,因此当您第二次访问它时,它是纯内存访问(1ms)。即使您只需要访问文件的非常小的部分,libc/kernel/controller优化也会访问相当大的块中的磁盘。您可以读取libc/OS/controller文档,尝试在这些块上对齐您的读取。

2) 如果使用流输入,请尝试使用直接的open/read/close函数:低级I/O的开销较小(显然)。没有什么比这更快的了,所以如果你仍然觉得这太慢,那你就有操作系统或硬件问题了。

因为看起来你有一个很好的基准,所以试着在你的fread呼叫中切换大小和计数。读取1000字节的1倍将比1000 x 1字节更快。

磁盘很慢,正如您所指出的,延迟来自第一次访问,即磁盘旋转并访问必要的扇区。你总是要一次性支付这笔费用。

使用内存映射IO可以稍微提高性能。请参阅mmap(Linux)或CreateFileMapping+MapViewOfFile(Windows)。

我已经优化了文件,以便尽可能少的部分必须读取

如果我错了,请纠正我,但关于正在优化的文件,我认为你的意思是你已经订购了这些部分,以尽量减少读取次数,而不是我要建议的。

此处受IO约束可能是由于寻道时间的原因,因此除了获得更快的存储介质外,您的选择也是有限的。

我有两个可能的想法:-

1) 压缩存储的数据,这可能会使您的读取时间稍快,但对查找时间仍然没有帮助。你必须测试一下这是否有益。

2) 如果相关,一旦检索到一个数据块,就将其移动到线程中,并在进行另一次读取时开始处理它。你可能已经在做了,但如果没有,我认为值得一提。