磁盘驱动器有两个(实际上是三个)限制其速度的因素:访问时间、顺序带宽和总线延迟/带宽。
您最感受的是访问时间。访问时间通常在毫秒范围内。在一个典型的硬盘上,必须进行一次查找需要超过 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 非常少。