mmap如何提高文件读取速度?

7
假设地址空间可以覆盖文件,我认为mmap只是分配一个与即将读取的文件大小相同的内存块,并创建它们对应块之间的1:1关系。但是,为什么这样做可以加速文件读取呢?似乎为了实际获取文件内容,您仍然需要访问磁盘并读取其中所有字节。
与malloc分配相同大小的内存并手动将整个文件读入malloc的区域相比,有何不同呢?

它并不会加速读取;相同数量的块必须从磁盘中读入。它只是从程序员的角度(有时)更方便。而mmap并不分配一块内存,它分配地址空间,实际数据稍后会在引用时缺页中断 - wildplasser
3个回答

10

mmap的工作方式略有不同,它是预测性的,并适应程序的访问模式。此外,可以通过madvise设置特定策略以进一步微调使用。

关于在需求分页环境中mmap的工作原理的更详尽讨论,请参见我在这里的回答:哪些段会受到写时复制的影响?,它还谈到了mmap的使用。

mmap是通过execve等程序执行的命脉。所以,你可以肯定它很快。顺便说一句,有趣的是,malloc实际上也使用匿名的mmap

但是,在这里讨论,特别要注意mmap和使用mallocread(2)时的“后备存储”(即分页磁盘)之间的区别。

通过使用mmap,内存区域的后备存储是文件本身。该区域直接映射到内核的文件系统缓冲区页[它们已经合并很长时间了]。因此,不需要像read(2)那样从内核文件系统缓冲区页复制到应用程序页。

当你使用malloc/read时,你仍然有上述页面,但是现在malloc的区域在分页/交换磁盘上有一个后备存储。因此,与mmap相比,有两倍多的页面缓冲区。正如我所提到的,当进行读取时,数据必须被复制到该区域中。

此外,进行大量读取的性能不佳。推荐的大小为每次约64 KB [取决于文件系统]。

当你进行大量读取时,你的程序在完成之前无法启动。如果文件的大小大于物理内存,则系统将读入您的malloc区域,并将先前的页面不断地写入分页磁盘以腾出空间,以便读取文件末尾附近的页面,直到整个文件被读入。

换句话说,应用程序正在等待(并且什么也没做),而这个大的预读正在发生。对于[例如]60 GB文件,启动时间将是可察觉的

如果您的文件真的足够大,您甚至会在分页磁盘上用尽空间(即malloc返回NULL)。

对于mmap,没有这样的问题。当您映射文件时,可以立即开始使用它。它将直接从区域的后备存储器中(再次强调,在文件系统中的文件)按需“错误转移”。如果您有(比如说)1 TB 的文件,mmap 可以很好地处理。
此外,您可以通过madvise(2)posix_madvise(2)来逐页或任何页面范围(包括整个文件)控制映射策略。madvise系统调用相对较轻,因此可以经常使用。它是一个提示,但不会执行会延迟应用程序的 I/O 操作。如果启动 I/O 以进行提示读取,则由内核作为后台活动完成。
您甚至可以告诉系统一个给定的页面很快就会需要(系统将其视为预取的提示),或者您可以告诉系统该页面不再需要(系统将释放页面缓冲区内存)。
您可以像“顺序访问”整个文件一样说话,这意味着系统将自动进行预读,以及释放不再需要的页面(即,如果您当前正在访问页面 N,则系统会在 N-k 之前释放任何页面)
当您执行read(2)时,无法告诉系统给定的内核 FS 页面缓冲区不再需要。它们将持续存在,直到物理 RAM 填满(或超过给定限制),这会给整个内存系统带来压力。
实际上,在使用read时,我看到用于 FS 缓冲区的内存量在应用程序转移到文件的不同部分或完全不同的文件之后仍然保持高水平。事实上,我曾经看到单个 I/O 密集型应用程序使用了如此多的缓冲区,以至于导致无关的(空闲)进程被其页面窃取并刷新到分页磁盘。当我停止 I/O 应用程序时,花费了几分钟的时间才能使 Firefox 分页并重新变得响应。
我进行了一些广泛的基准测试,比较常规的读取和 mmap。从中可以看出,mmap 可以提高某些应用程序的速度。
请参见我的答案:read line by line in the most efficient way *platform specific* 在此之前,我对 mmap 的好处持怀疑态度,但基准测试表明 mmap 是胜利者。
此外,如果您正在执行read(2)(为了加快速度)与fgets相比,如果给定行跨越读取缓冲区边界(即,您的缓冲区的最后50个字符具有80个字符行的前50个字节),则可能会陷入缓冲区移位问题。
请在链接页面的注释中注意,有另一个链接指向 pastebin 上更新版本的基准测试程序和结果。由于以上 Stack Overflow 回答中无法发布过大的基准测试结果和比较各种 madvise 选项的情况,因此需要该链接。请保留 HTML 标记。

为什么 mmap 更快呢?它似乎需要逐个访问块,而其他流式函数一次获取大块数据。在我看来,mmap 需要更多的时间来旋转磁盘和读取数据。 - OneZero
@OneZero 我在之前的回答中添加了一个新链接[我之前忘记了]。我在这里的回答中也添加了很多关于mmap如何工作的解释。它的工作方式并不像你想象的那样简单。它要复杂得多。此外,我还添加了与malloc/read方法的比较--实际上它并不像人们认为的那样优秀。 - Craig Estey
真是个好答案。 - David C. Rankin
@DavidC.Rankin 你好,David。谢谢。如果你喜欢这个答案,你应该看看https://dev59.com/k1kT5IYBdhLWcg3wALDw#39185831 它刚刚获得了“不错的答案”徽章。_法律声明:_我 鼓励串联投票。 - Craig Estey
我总是喜欢深入了解分页的内部工作原理,并尽可能避免使用用户空间来进行优化。我曾经在文件复制例程计时中简要地涉足mmap领域。我了解了更多关于sendfile内核空间复制的知识(我理解它相当于mmap/memcpy),但没有深入研究mmap本身。感谢提供链接和答案。 - David C. Rankin
如果您喜欢使用mmap来提高性能,请务必查看我的答案:https://dev59.com/MJDea4cB1Zd3GeqPiesP#33620968 它包含完整的代码和基准测试[具有令人惊讶的结果]。此外,请确保向下滚动评论以找到最终代码版本的pastebin链接。 - Craig Estey

5
我对此很好奇,因此尝试了针对文件大小为1、2、4、8等的整个文件读取基准测试,一次使用mmap(M),一次使用read(R)(理论上只需使用fstat大小调用一次,但如果该调用返回部分结果,它将重试)。在读取/映射后,以不可优化的方式访问了每个映射/读取页面的一个字节。
以下是我的结果:
Size   M(µs)   R(µs)
1      9.5     4.2
2      10.8    4.5
4      8.4     3.8
8      8.6     3.8
16     7.3     4
32     7.8     3.5
64     8.3     3.9
128    9.2     4.6
256    8.6     4.7
512    10.6    5.1
1.0Ki  9.8     4.7
2.0Ki  10.1    5.4
4.0Ki  10.5    5.6
8.0Ki  10.4    6.9
16Ki   9.9     10
32Ki   14.4    12.8
64Ki   16.1    23.7
128Ki  28.1    41.1
256Ki  34.5    82.4
512Ki  57.9    154.6
1.0Mi  103.5   325.8
2.0Mi  188.5   919.8
4.0Mi  396.3   1963.2
8.0Mi  798.8   3885
16Mi   1611.4  7660.2
32Mi   3207.4  23040.2
64Mi   6712.1  84491.9

看起来在大约 16Ki 之前,read 要快两倍左右。从那时起,mmap 就开始大幅领先了(对于 64MiB 文件,优势高达 12 倍)。

(在我的笔记本电脑上使用 Linux 3.19 进行测试,对同一文件进行了 $10^4$ 次重复读取。)


0

并不是这样的。具体来说,当您调用mmap()时,它不会将整个文件加载到内存中,以某种方式加快访问速度。相反,它映射文件,也就是说,在内存中创建了一个文件索引(我使用术语不严谨,请容忍),以便在尝试读取/写入该“键”时触发页面错误。因此,净效果是您拥有文件的简单接口和一种类似于文件内容的惰性加载。

我可以继续说下去,但其他人做得更好。例如,请参见此处


似乎使用延迟加载时,内存映射只会变得更慢?因为现在每次访问页面都必须旋转磁盘并读取块,而不是一次性读取大量页面。是这样吗? - OneZero

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接