5

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

我有 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)。在此调用之后,我必须从文件的不同部分读取双精度值,但我无法避免这种情况。

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

如果我使用内存映射,第一次读取总是很慢。如果我再重复阅读本节的内容,它会很快变亮。然后,当我从不同的部分阅读时,第一次速度很慢,但第二次速度很快。

我不知道为什么会这样。

有人对我有更多的想法吗?谢谢你。

4

6 回答 6

6

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

您最感受的是访问时间。访问时间通常在毫秒范围内。在一个典型的硬盘上,必须进行一次查找需要超过 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 在现代计算机上应该不是很大的障碍。然而,将200 MiB 读fread入分配的缓冲区可能是禁止的(这取决于您的目标架构,以及您的程序正在做什么)。不过不用担心,您已经有了解决问题的最佳方法:内存映射。
虽然内存映射不是“魔法加速器”,但它仍然尽可能接近“魔法”。

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

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

Linux 具有预系统调用,可让您预取数据。不幸的是,它会一直阻塞,直到获取数据,这可能不是您想要的(因此您必须为此使用额外的线程)。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 非常少。

于 2013-06-25T11:33:30.127 回答
4

(移至OP要求的答案)

您不能更快地从文件中读取(没有魔术标志可以说“读取速度更快”)。您的硬件有问题,或者 200mS 是应该花费多长时间

于 2013-06-25T07:00:04.910 回答
1

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

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

于 2013-06-25T07:10:27.680 回答
0

看起来你有一个很好的基准,试着在你的 fread 调用中切换大小和计数。读取 1 次 1000 字节将比 1000 x 1 字节快。

于 2013-06-25T05:33:37.277 回答
0

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

如果我错了,请纠正我,但关于正在优化的文件,我假设您的意思是您已经订购了这些部分以尽量减少发生的读取次数,而不是我将建议的内容。

在这里受到 IO 的约束可能是由于查找时间,所以除了获得更快的存储介质之外,您的选择是有限的。

我有两个可能的想法是: -

1)压缩存储的数据,这可能会使您的读取时间稍快一些,但仍无助于查找时间。你必须测试这是否有好处。

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

于 2013-06-25T08:28:26.920 回答
0

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

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

于 2013-06-25T06:06:56.197 回答