这里已经有很多很好的答案,涵盖了许多突出的问题,所以我只是补充几个我在上面没有直接提到的问题。也就是说,这个答案不应该被认为是利弊的综合,而应该是这里其他答案的补充。
Mmap看起来就像魔法
如果文件已经完全cached1作为基线2,mmap可能看起来很像魔术:
Mmap只需要一个系统调用来映射整个文件(可能),之后就不需要更多的系统调用了。
Mmap不需要将文件数据从内核复制到用户空间。
mmap允许您以“内存”的形式访问文件,包括使用您可以针对内存做的任何高级技巧来处理它,例如编译器自动向量化、SIMD intrinsic、预取、优化的内存解析例程、OpenMP等。
在文件已经在缓存中的情况下,这似乎是不可能的:您只是直接访问内核页缓存作为内存,它不能比这更快。
是的,它可以。
Mmap其实并不神奇,因为……
Mmap仍然执行逐页工作
mmap与read(2)的主要隐藏成本(实际上是读取块的可比较的操作系统级系统调用)是,使用mmap,您将需要为新映射中访问的每一个4K页面做“一些工作”,即使它可能被页面故障机制隐藏。
举个例子,一个典型的实现只需要映射整个文件就需要故障插入,所以100gb / 4K = 2500万个错误来读取100gb的文件。现在,这些将是小错误,但2500万页错误仍然不是超级快。在最好的情况下,一个小故障的成本可能在100纳米。
mmap严重依赖于TLB性能
Now, you can pass MAP_POPULATE to mmap to tell it to set up all the page tables before returning, so there should be no page faults while accessing it. Now, this has the little problem that it also reads the entire file into RAM, which is going to blow up if you try to map a 100GB file - but let's ignore that for now3. The kernel needs to do per-page work to set up these page tables (shows up as kernel time). This ends up being a major cost in the mmap approach, and it's proportional to the file size (i.e., it doesn't get relatively less important as the file size grows)4.
最后,即使在用户空间中访问这样的映射也不是完全免费的(与不是来自基于文件的mmap的大内存缓冲区相比)——即使一旦设置了页表,从概念上讲,对新页的每次访问都将引起TLB miss。因为mmap文件意味着使用页缓存和它的4K页面,所以对于100GB文件,您又会引起这个成本2500万次。
Now, the actual cost of these TLB misses depends heavily on at least the following aspects of your hardware: (a) how many 4K TLB enties you have and how the rest of the translation caching works performs (b) how well hardware prefetch deals with with the TLB - e.g., can prefetch trigger a page walk? (c) how fast and how parallel the page walking hardware is. On modern high-end x86 Intel processors, the page walking hardware is in general very strong: there are at least 2 parallel page walkers, a page walk can occur concurrently with continued execution, and hardware prefetching can trigger a page walk. So the TLB impact on a streaming read load is fairly low - and such a load will often perform similarly regardless of the page size. Other hardware is usually much worse, however!
Read()避免了这些陷阱
在C、c++和其他语言中,read()系统调用通常是“块读”类型调用的基础,它有一个每个人都很清楚的主要缺点:
每个N字节的read()调用必须从内核复制N字节到用户空间。
另一方面,它避免了上述大部分成本——您不需要将2500万4K页面映射到用户空间。您通常可以在用户空间中malloc一个单独的小缓冲区,并在所有的read调用中重复使用它。在内核方面,几乎不存在4K页面或TLB丢失的问题,因为所有的RAM通常使用几个非常大的页面(例如,x86上的1gb页面)进行线性映射,因此页面缓存中的底层页面在内核空间中被非常有效地覆盖。
所以基本上你可以通过下面的比较来确定对于一个大文件的单次读取,哪个更快:
mmap方法所隐含的每页额外工作是否比使用read()将文件内容从内核复制到用户空间所隐含的每字节工作更昂贵?
在许多系统中,它们实际上是近似平衡的。请注意,每一个都可以使用完全不同的硬件和操作系统堆栈属性进行扩展。
特别是,mmap方法在以下情况下变得相对更快:
该操作系统具有快速的小故障处理,特别是小故障膨胀优化,如故障规避。
该操作系统具有良好的MAP_POPULATE实现,可以有效地处理大型映射,例如,底层页面在物理内存中是连续的。
硬件具有较强的页面转换性能,如大tlb、快速的二级tlb、快速并行的走页器、与翻译良好的预取交互等。
... 而read()方法在以下情况下会变得相对更快:
read()系统调用具有良好的复制性能。例如,内核端良好的copy_to_user性能。
内核有一种有效的(相对于用户区域)映射内存的方法,例如,在硬件支持下只使用几个大页面。
内核具有快速的系统调用,并且有一种方法可以跨系统调用保持内核TLB条目。
上述硬件因素在不同的平台上有很大的差异,甚至在同一个家族中(例如,在x86代中,特别是在市场细分中),并且肯定在不同的架构中(例如,ARM vs x86 vs PPC)。
操作系统因素也在不断变化,双方的各种改进导致一种方法或另一种方法的相对速度有了很大的飞跃。最近的一份名单包括:
添加了上面描述的绕过错误的功能,这确实有助于在没有MAP_POPULATE的情况下使用mmap。
在arch/x86/lib/copy_user_64中增加了快速copy_to_user方法。S,例如,使用REP MOVQ时,它是快速的,这真的有助于read()情况。
在Spectre和Meltdown之后更新
The mitigations for the Spectre and Meltdown vulnerabilities considerably increased the cost of a system call. On the systems I've measured, the cost of a "do nothing" system call (which is an estimate of the pure overhead of the system call, apart from any actual work done by the call) went from about 100 ns on a typical modern Linux system to about 700 ns. Furthermore, depending on your system, the page-table isolation fix specifically for Meltdown can have additional downstream effects apart from the direct system call cost due to the need to reload TLB entries.
与基于mmap的方法相比,所有这些都是基于read()的方法的相对缺点,因为read()方法必须对每个“缓冲区大小”的数据进行一次系统调用。您不能任意增加缓冲区大小来摊销这一成本,因为使用大缓冲区通常性能更差,因为您超过了L1大小,因此不断遭受缓存丢失。
另一方面,使用mmap,您可以用MAP_POPULATE映射一个大内存区域,并有效地访问它,只需要一个系统调用。
1这或多或少也包括文件一开始没有完全缓存的情况,但操作系统预读足够好,使它看起来是这样的(即,页面通常在你想要它的时候被缓存)。这是一个微妙的问题,因为预读在mmap和read调用之间的工作方式通常是非常不同的,并且可以通过“advise”调用进一步调整,如2中所述。
2…因为如果文件没有被缓存,你的行为将完全由IO考虑决定,包括你的访问模式与底层硬件的一致程度——你所有的努力都应该确保这样的访问是尽可能一致的,例如通过使用madvise或fadvise调用(以及任何你可以改进访问模式的应用程序级别的更改)。
3你可以解决这个问题,例如,在一个较小的窗口中按顺序映射,比如100mb。
4事实上,MAP_POPULATE方法(至少在某些硬件/操作系统组合中)只比不使用它稍微快一点,可能是因为内核使用了faultaround——因此小故障的实际数量减少了16倍左右。