题
我正在开发一个程序,该程序将处理大小可能为 100GB 或更大的文件。这些文件包含可变长度记录集。我已经启动并运行了第一个实现,现在正在寻求提高性能,特别是在更有效地执行 I/O 方面,因为输入文件被扫描了很多次。
有没有使用的经验法则 mmap()
与通过 C++ 分块读取相比 fstream
图书馆?我想做的是将大块从磁盘读取到缓冲区中,处理缓冲区中的完整记录,然后读取更多内容。
这 mmap()
代码可能会变得非常混乱,因为 mmap
块需要位于页面大小的边界上(我的理解),并且记录可能会跨越页面边界。和 fstream
s,我可以只查找记录的开头并再次开始读取,因为我们不限于读取位于页面大小边界上的块。
在不首先实际编写完整的实现的情况下,如何在这两个选项之间做出决定?任何经验法则(例如, mmap()
快 2 倍)还是简单的测试?
解决方案
我试图找到有关 Linux 上 mmap/read 性能的最终结论,我发现了一篇不错的文章(关联)在 Linux 内核邮件列表上。这是从2000年开始的,所以从那以后内核中的IO和虚拟内存有了很多改进,但是它很好地解释了原因 mmap
或者 read
可能会更快或更慢。
- 致电
mmap
开销比read
(就像epoll
开销比poll
, ,其开销比read
)。在某些处理器上,更改虚拟内存映射是一项相当昂贵的操作,其原因与不同进程之间的切换成本高昂相同。 - IO系统已经可以使用磁盘缓存,所以如果你读取一个文件,无论你使用什么方法,你都会命中缓存或错过它。
然而,
- 内存映射对于随机访问通常更快,特别是当您的访问模式稀疏且不可预测时。
- 内存映射允许您 保持 使用缓存中的页面直到完成。这意味着,如果您长时间大量使用某个文件,然后关闭并重新打开它,页面仍会被缓存。和
read
, ,您的文件可能很久以前就已从缓存中清除。如果您使用文件并立即丢弃它,则这不适用。(如果你尝试mlock
页面只是为了将它们保存在缓存中,您正在尝试智取磁盘缓存,而这种愚蠢的行为很少有助于系统性能)。 - 直接读取文件非常简单快捷。
mmap/read 的讨论让我想起另外两个性能讨论:
一些 Java 程序员惊讶地发现非阻塞 I/O 通常比阻塞 I/O 慢,如果您知道非阻塞 I/O 需要进行更多系统调用,那么这是完全有道理的。
其他一些网络程序员惊讶地发现
epoll
通常比poll
, ,如果您知道管理的话,这是完全有道理的epoll
需要进行更多的系统调用。
结论: 如果您随机访问数据、长期保留数据,或者您知道可以与其他进程共享数据,请使用内存映射(MAP_SHARED
如果没有实际的分享就不是很有趣)。如果按顺序访问数据,则正常读取文件,或者在读取后丢弃数据。如果任何一种方法都可以让你的程序变得不那么复杂,那就这样做 那. 。对于许多现实世界的情况,如果不测试您的实际应用程序而不是基准测试,就没有确定的方法可以证明速度更快。
(很抱歉这个问题被破坏了,但我一直在寻找答案,而这个问题一直出现在谷歌结果的顶部。)
其他提示
主要的性能成本是磁盘 I/O。“mmap()”肯定比 istream 快,但差异可能并不明显,因为磁盘 I/O 将主导您的运行时间。
我尝试了 Ben Collins 的代码片段(见上文/下文)来测试他的断言“mmap() 是 方式 更快”并且没有发现任何可测量的差异。请参阅我对他的回答的评论。
我当然会 不是 建议依次单独映射每个记录,除非您的“记录”很大 - 这会非常慢,需要为每个记录进行 2 次系统调用,并且可能会丢失磁盘内存缓存中的页面......
在你的情况下,我认为 mmap()、istream 和低级 open()/read() 调用都大致相同。在这些情况下我会推荐 mmap() :
- 文件内存在随机访问(非顺序),并且
- 整个内容可以轻松地放入内存中,或者文件内存在引用局部性,以便可以映射某些页面并映射出其他页面。这样操作系统就可以利用可用的 RAM 来发挥最大的作用。
- 或者,如果多个进程正在读取/处理同一个文件,那么 mmap() 就非常有用,因为这些进程都共享相同的物理页。
(顺便说一句 - 我喜欢 mmap()/MapViewOfFile())。
映射是 方式 快点。您可以编写一个简单的基准来向自己证明这一点:
char data[0x1000];
std::ifstream in("file.bin");
while (in)
{
in.read(data, 0x1000);
// do something with data
}
相对:
const int file_size=something;
const int page_size=0x1000;
int off=0;
void *data;
int fd = open("filename.bin", O_RDONLY);
while (off < file_size)
{
data = mmap(NULL, page_size, PROT_READ, 0, fd, off);
// do stuff with data
munmap(data, page_size);
off += page_size;
}
显然,我省略了细节(例如,如果您的文件不是文件的倍数,则如何确定何时到达文件末尾) page_size
, ,例如),但实际上不应该比这更复杂。
如果可以的话,您可以尝试将数据分解为多个文件,这些文件可以全部而不是部分地进行 mmap() 编辑(更简单)。
几个月前,我为 boost_iostreams 实现了一个半成品的滑动窗口 mmap() 流类,但没有人关心,我忙于其他事情。最不幸的是,我几周前删除了旧的未完成项目的档案,而那是受害者之一:-(
更新:我还应该补充一点,这个基准测试在 Windows 中看起来会完全不同,因为 Microsoft 实现了一个漂亮的文件缓存,它可以完成您首先使用 mmap 执行的大部分操作。也就是说,对于经常访问的文件,您可以只执行 std::ifstream.read() ,它会和 mmap 一样快,因为文件缓存已经为您完成了内存映射,并且它是透明的。
最终更新:大家看:跨越操作系统、标准库、磁盘和内存层次结构的许多不同平台组合,我不能肯定地说系统调用 mmap
, ,被视为一个黑匣子,总是比 read
. 。这并不完全是我的意图,即使我的话可以这样解释。 最终,我的观点是内存映射 I/O 通常比基于字节的 I/O 更快;这仍然是事实. 。如果您通过实验发现两者之间没有区别,那么对我来说唯一合理的解释是您的平台以有利于调用性能的方式在幕后实现内存映射 read
. 。绝对确定您正在以可移植方式使用内存映射 I/O 的唯一方法是使用 mmap
. 。如果您不关心可移植性并且可以依赖目标平台的特定特征,那么使用 read
可能是合适的,而不会显着牺牲任何性能。
编辑以清理答案列表:@jbl:
滑动窗口MMAP听起来很有趣。您能再说更多吗?
当然 - 我正在为 Git 编写一个 C++ 库(如果你愿意的话,可以是 libgit++),我遇到了与此类似的问题:我需要能够打开大(非常大)的文件,并且性能不能太差(就像使用 std::fstream
).
Boost::Iostreams
已经有一个mapped_file源,但问题是它是 mmap
ping 整个文件,这将您限制为 2^(wordsize)。在 32 位机器上,4GB 不够大。期望拥有这并不是没有道理的 .pack
Git 中的文件变得比这个大得多,所以我需要分块读取文件,而不需要求助于常规文件 I/O。在以下的掩护下 Boost::Iostreams
, ,我实现了一个Source,这或多或少是之间交互的另一种视图 std::streambuf
和 std::istream
. 。您也可以通过继承来尝试类似的方法 std::filebuf
变成一个 mapped_filebuf
同样,继承 std::fstream
进入 a mapped_fstream
. 。两者之间的互动是很难正确处理的。 Boost::Iostreams
已经为你完成了一些工作,并且它还提供了过滤器和链的钩子,所以我认为以这种方式实现它会更有用。
这里已经有很多很好的答案,涵盖了许多要点,因此我将添加一些我在上面没有直接解决的问题。也就是说,这个答案不应被视为对利弊的综合,而应被视为对此处其他答案的补充。
mmap 看起来很神奇
以文件已完全缓存的情况为例1 作为基线2, mmap
可能看起来很像 魔法:
mmap
只需要 1 次系统调用即可(可能)映射整个文件,之后不再需要系统调用。mmap
不需要将文件数据从内核复制到用户空间。mmap
允许您“作为内存”访问文件,包括使用可以针对内存执行的任何高级技巧来处理它,例如编译器自动向量化, 单指令多数据流 内在函数、预取、优化的内存解析例程、OpenMP 等。
在文件已经在缓存中的情况下,似乎无法击败:您只需直接将内核页面缓存作为内存进行访问,并且速度不会比这更快。
嗯,可以。
mmap 实际上并不神奇,因为......
mmap 仍然执行每页工作
主要隐性成本 mmap
与 read(2)
(这实际上是可比较的操作系统级系统调用 阅读块)是与 mmap
您需要为用户空间中的每个 4K 页面做“一些工作”,即使它可能被页面错误机制隐藏。
举个例子,一个典型的实现 mmap
整个文件需要发生故障,因此 100 GB / 4K = 2500 万个故障才能读取 100 GB 的文件。现在,这些将是 小故障, ,但是 250 亿个页面错误仍然不会很快。在最好的情况下,一个小故障的成本可能是数百纳秒。
mmap 严重依赖 TLB 性能
现在,你可以通过 MAP_POPULATE
到 mmap
告诉它在返回之前设置所有页表,这样访问它时就不应该出现页面错误。现在,这有一个小问题,它还将整个文件读取到 RAM 中,如果您尝试映射 100GB 文件,RAM 将会崩溃 - 但现在让我们忽略它3. 。内核需要做的事情 每页工作 设置这些页表(显示为内核时间)。这最终成为了一项主要成本 mmap
方法,并且它与文件大小成正比(即,随着文件大小的增长,它不会变得相对不那么重要)4.
最后,即使在用户空间中,访问此类映射也不是完全免费的(与并非源自基于文件的大内存缓冲区相比) mmap
) - 即使页表设置完毕,从概念上讲,每次访问新页都会导致 TLB 未命中。自从 mmap
处理一个文件意味着使用页面缓存及其 4K 页面,对于 100GB 的文件,您会再次产生 2500 万倍的成本。
现在,这些 TLB 未命中的实际成本在很大程度上取决于硬件的至少以下方面:(a) 您拥有多少个 4K TLB 实体,以及翻译缓存的其余部分如何执行 (b) 硬件预取处理 TLB 的效果如何 - 例如,预取能否触发页面遍历?(c) 页面遍历硬件的速度和并行程度。在现代高端 x86 Intel 处理器上,页面行走硬件通常非常强大:至少有 2 个并行页遍历器,页遍历可以与继续执行同时发生,并且硬件预取可以触发页遍历。所以 TLB 对 流媒体 读取负载相当低 - 并且无论页面大小如何,这样的负载通常都会执行类似的操作。然而,其他硬件通常要差得多!
read() 避免了这些陷阱
这 read()
系统调用通常是 C、C++ 和其他语言中提供的“块读取”类型调用的基础,它有一个每个人都清楚的主要缺点:
- 每一个
read()
N 字节的调用必须将 N 字节从内核复制到用户空间。
另一方面,它避免了上述大部分成本 - 您不需要将 2500 万个 4K 页面映射到用户空间。通常你可以 malloc
用户空间中的单个缓冲区小缓冲区,并为所有您重复使用它 read
来电。在内核方面,几乎不存在 4K 页面或 TLB 未命中的问题,因为所有 RAM 通常都是使用一些非常大的页面(例如 x86 上的 1 GB 页面)进行线性映射,因此页面缓存中的底层页面被覆盖在内核空间中非常有效。
因此,基本上可以通过以下比较来确定单个读取大文件的速度更快:
是否隐含了额外的每页工作量 mmap
方法比使用隐含的将文件内容从内核复制到用户空间的每字节工作成本更高 read()
?
在许多系统上,它们实际上是近似平衡的。请注意,每一种都可以根据硬件和操作系统堆栈的完全不同的属性进行扩展。
特别是, mmap
在以下情况下,方法变得相对更快:
- 该操作系统具有快速的小故障处理能力,尤其是小故障批量优化(例如故障规避)。
- 该操作系统有一个很好的
MAP_POPULATE
例如,在底层页面在物理内存中连续的情况下,可以有效地处理大型映射的实现。 - 硬件具有强大的页翻译性能,如大型TLB、快速的二级TLB、快速并行的page-walker、良好的预取与翻译交互等。
...而 read()
在以下情况下,方法变得相对更快:
- 这
read()
syscall 具有良好的复制性能。例如,好copy_to_user
内核方面的性能。 - 内核有一种有效的(相对于用户态)映射内存的方式,例如,仅使用几个具有硬件支持的大页面。
- 内核具有快速的系统调用和跨系统调用保存内核 TLB 条目的方法。
上述硬件因素各不相同 疯狂地 跨不同平台,甚至在同一个系列内(例如,x86 代,特别是细分市场),并且肯定跨架构(例如,ARM vs x86 vs PPC)。
操作系统因素也在不断变化,双方的各种改进导致一种方法或另一种方法的相对速度大幅跃升。最近的一份清单包括:
- 如上所述,添加故障排除,这确实有助于
mmap
情况下没有MAP_POPULATE
. - 添加快速路径
copy_to_user
中的方法arch/x86/lib/copy_user_64.S
, ,例如,使用REP MOVQ
当它很快时,这确实有帮助read()
案件。
幽灵和崩溃后的更新
Spectre 和 Meltdown 漏洞的缓解措施大大增加了系统调用的成本。在我测量过的系统上,“不执行任何操作”的系统调用(这是对系统调用的纯粹开销的估计,不包括调用完成的任何实际工作)的成本在典型的系统上约为 100 纳秒。现代 Linux 系统大约需要 700 ns。此外,根据您的系统, 页表隔离 除了由于需要重新加载 TLB 条目而导致的直接系统调用成本之外,专门针对 Meltdown 的修复还会产生额外的下游影响。
所有这些对于 read()
基于方法相比 mmap
基于方法,因为 read()
方法必须为每个“缓冲区大小”的数据进行一次系统调用。您不能任意增加缓冲区大小来分摊此成本,因为使用大缓冲区通常会表现更差,因为您超过了 L1 大小,因此不断遭受缓存未命中。
另一方面,与 mmap
, ,你可以映射到一个大的内存区域 MAP_POPULATE
并且只需一次系统调用即可有效地访问它。
1 这或多或少还包括文件一开始没有完全缓存的情况,但操作系统的预读足以使其看起来如此(即,页面通常在您想要的时间被缓存)它)。但这是一个微妙的问题,因为预读的工作方式通常在不同的情况下有很大不同。 mmap
和 read
调用,并且可以通过“建议”调用进一步调整,如中所述 2.
2 ...因为如果文件是 不是 缓存后,您的行为将完全由 IO 问题主导,包括您的访问模式对底层硬件的支持程度 - 并且您所有的努力都应该是确保此类访问尽可能地支持,例如通过使用 madvise
或者 fadvise
调用(以及您可以进行的任何应用程序级别更改以改进访问模式)。
3 例如,您可以通过依次解决这个问题 mmap
在较小尺寸的窗口中,例如 100 MB。
4 事实上,事实证明 MAP_POPULATE
方法(至少一种硬件/操作系统组合)仅比不使用它快一点,可能是因为内核正在使用 故障解决方法 - 因此小故障的实际数量减少了 16 倍左右。
很抱歉 Ben Collins 丢失了他的滑动窗口 mmap 源代码。如果能在 Boost 中使用那就太好了。
是的,映射文件要快得多。您本质上是使用操作系统虚拟内存子系统来关联内存与磁盘,反之亦然。这样想:如果操作系统内核开发人员可以使其更快,他们就会的。因为这样做会让一切变得更快:数据库、启动时间、程序加载时间等等。
滑动窗口方法实际上并不困难,因为可以一次映射多个连续页面。因此,只要单个记录中最大的一条记录能够装入内存,记录的大小并不重要。重要的是管理簿记。
如果记录不是从 getpagesize() 边界开始,则映射必须从上一页开始。映射区域的长度从记录的第一个字节(如有必要,向下舍入到 getpagesize() 的最接近的倍数)到记录的最后一个字节(向上舍入到 getpagesize() 的最接近的倍数)。处理完一条记录后,您可以 unmap() 它,然后继续处理下一条记录。
这一切在 Windows 下也可以正常工作,使用 CreateFileMapping() 和 MapViewOfFile() (以及 GetSystemInfo() 来获取 SYSTEM_INFO.dwAllocationGranularity --- 而不是 SYSTEM_INFO.dwPageSize)。
mmap 应该更快,但我不知道快多少。这很大程度上取决于您的代码。如果您使用 mmap,最好立即 mmap 整个文件,这将使您的生活变得更加轻松。一个潜在的问题是,如果您的文件大于 4GB(或者实际上限制较低,通常为 2GB),您将需要 64 位架构。因此,如果您使用的是 32 位环境,您可能不想使用它。
话虽如此,可能有更好的途径来提高性能。你说 输入文件被扫描多次, ,如果你可以一次性读出它然后完成它,那可能会快得多。
我同意 mmap 文件 I/O 会更快,但是在对代码进行基准测试时,反例不应该是 有些 优化?
本·柯林斯写道:
char data[0x1000];
std::ifstream in("file.bin");
while (in)
{
in.read(data, 0x1000);
// do something with data
}
我建议也尝试:
char data[0x1000];
std::ifstream iifle( "file.bin");
std::istream in( ifile.rdbuf() );
while( in )
{
in.read( data, 0x1000);
// do something with data
}
除此之外,您还可以尝试使缓冲区大小与一页虚拟内存的大小相同,以防 0x1000 不是您计算机上一页虚拟内存的大小......恕我直言,mmap 文件 I/O 仍然获胜,但这应该会让事情变得更接近。
也许您应该预处理文件,因此每个记录都位于单独的文件中(或者至少每个文件都是可映射的大小)。
另外,在继续下一条记录之前,您是否可以对每条记录执行所有处理步骤?也许这可以避免一些 IO 开销?
在我看来,使用 mmap() “只是”减轻了开发人员编写自己的缓存代码的负担。在简单的“一次读取文件”的情况下,这并不困难(尽管 mlbrock 指出您仍然将内存副本保存到进程空间中),但是如果您要在文件中来回移动或跳过位等等,我相信内核开发人员已经 大概 实现缓存比我做得更好......
我记得几年前将一个包含树结构的巨大文件映射到内存中。与普通反序列化相比,我对速度感到惊讶,普通反序列化涉及内存中的大量工作,例如分配树节点和设置指针。因此,实际上,我正在比较对MMAP(或Windows上的对应方)的单个呼叫与对操作员的许多(许多)呼叫的新呼叫和构造函数。对于此类任务,与反序列化相比,mmap 是无与伦比的。当然,人们应该为此研究一下 boosts 可重定位指针。
这听起来像是多线程的一个很好的用例......我认为您可以很容易地设置一个线程来读取数据,而其他线程则处理它。这可能是一种显着提高感知性能的方法。只是一个想法。
我认为 mmap 的最大优点是异步读取的潜力:
addr1 = NULL;
while( size_left > 0 ) {
r = min(MMAP_SIZE, size_left);
addr2 = mmap(NULL, r,
PROT_READ, MAP_FLAGS,
0, pos);
if (addr1 != NULL)
{
/* process mmap from prev cycle */
feed_data(ctx, addr1, MMAP_SIZE);
munmap(addr1, MMAP_SIZE);
}
addr1 = addr2;
size_left -= r;
pos += r;
}
feed_data(ctx, addr1, r);
munmap(addr1, r);
问题是我找不到正确的 MAP_FLAGS 来提示该内存应尽快从文件同步。我希望 MAP_POPULATE 为 mmap 提供正确的提示(即它不会在调用返回之前尝试加载所有内容,而是以异步方式加载所有内容。与 feed_data)。至少使用此标志可以提供更好的结果,即使手册指出自 2.6.23 起如果没有 MAP_PRIVATE 它什么也不做。