jbl*_*jbl 172 c++ file-io fstream mmap
我正在开发一个程序,该程序将处理可能大小为100GB或更大的文件.这些文件包含一组可变长度记录.我已经启动并运行了第一个实现,现在我正在寻求提高性能,特别是在输入文件被多次扫描时更有效地进行I/O.
mmap()
通过C++的fstream
库使用和读取块有经验吗?我想做的是从磁盘读取大块到缓冲区,从缓冲区处理完整记录,然后阅读更多.
该mmap()
代码可能会变得非常凌乱,因为mmap
"d块需要躺在页大小的边界(我的理解)和记录可能潜在般划过页面边界.使用fstream
s,我可以寻找记录的开头并再次开始阅读,因为我们不仅限于阅读位于页面大小边界的块.
如何在不实际编写完整实现的情况下决定这两个选项?任何经验法则(例如,mmap()
快2倍)或简单测试?
Die*_*Epp 193
我试图在Linux上找到关于mmap/read性能的最后一句话,我在Linux内核邮件列表上遇到了一个很好的帖子(链接).它是从2000年开始的,因此从那时起内核中的IO和虚拟内存已经有了很多改进,但它很好地解释了为什么mmap
或者read
可能更快或更慢的原因.
mmap
具有更多的开销read
(就像epoll
有更多的开销,比poll
有更多的开销read
).在某些处理器上更改虚拟内存映射是一项非常昂贵的操作,原因与不同进程之间的切换成本高昂相同.然而,
read
,您的文件可能早已从缓存中刷新.如果您使用文件并立即丢弃它,则不适用.(如果您尝试将mlock
页面保留在缓存中,那么您正试图超越磁盘缓存,这种类型的漏洞很少有助于系统性能).对mmap/read的讨论让我想起了另外两个性能讨论:
一些Java程序员惊讶地发现非阻塞I/O通常比阻塞I/O慢,如果您知道非阻塞I/O需要进行更多的系统调用,这就非常有意义.
其他一些网络程序员感到震惊的epoll
是,通常比较慢poll
,如果你知道管理epoll
需要进行更多的系统调用,那就非常有意义了.
结论:如果您随机访问数据,长时间保存数据,或者如果您知道可以与其他进程共享数据,则使用内存映射(MAP_SHARED
如果没有实际共享,则不是很有趣).如果您按顺序访问数据或在读取后丢弃数据,则正常读取文件.如果任一方法,使你的程序那么复杂,做那个.对于许多现实世界的情况,没有确定的方法来显示一个更快,而不测试您的实际应用程序而不是基准测试.
(对不起,necro'ing这个问题,但我一直在寻找答案,这个问题一直想出在谷歌搜索结果的顶部.)
Tim*_*per 44
主要的性能成本是磁盘i/o."mmap()"肯定比istream快,但差异可能不明显,因为磁盘i/o将控制你的运行时间.
我试着本柯林斯的代码段(见上文/下文),以测试他断言,"mmap()的是方式更快",并没有发现可测量的差异.看看我对他的回答的评论.
除非你的"记录"很大,否则我肯定不会单独推荐mmap'ing每个记录 - 这将非常慢,每个记录需要2个系统调用,可能会丢失磁盘内存缓存中的页面.... .
在你的情况下,我认为mmap(),istream和低级open()/ read()调用都将大致相同.我建议在这些情况下使用mmap():
(顺便说一句 - 我喜欢mmap()/ MapViewOfFile()).
Ben*_*ins 42
MMAP是方式更快.您可以编写一个简单的基准来向自己证明:
char data[0x1000];
std::ifstream in("file.bin");
while (in)
{
in.read(data, 0x1000);
// do something with data
}
Run Code Online (Sandbox Code Playgroud)
与:
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;
}
Run Code Online (Sandbox Code Playgroud)
很明显,我遗漏了细节(例如,如果你的文件不是文件的倍数,如何确定何时到达文件的末尾page_size
),但它确实不应该比这复杂得多.
如果可以,您可以尝试将数据分解为多个文件,这些文件可以是mmap() - 整体而不是部分(更简单).
几个月前,我对boost_iostreams的滑动窗口mmap() - ed流类进行了半生不熟的实现,但没有人关心,我忙于其他的东西.最不幸的是,几个星期前我删除了一个旧的未完成项目的档案,这是受害者之一:-(
更新:我还应该添加一个警告,即这个基准测试在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 ^(单词大小).在32位机器上,4GB不够大.期望.pack
Git中的文件变得比那些大得多并不是不合理的,所以我需要以块的形式读取文件而不需要使用常规文件i/o.在Boost::Iostreams
我的封面下,我实现了一个Source,它或多或少地反映了std::streambuf
和之间的相互作用std::istream
.您也可以尝试类似的方法,只需继承std::filebuf
到a mapped_filebuf
,同样地,继承std::fstream
到a mapped_fstream
.这是两者之间的相互作用,很难做到正确. Boost::Iostreams
有一些工作为你完成,它还提供了过滤器和链的钩子,所以我认为以这种方式实现它会更有用.
Bee*_*ope 32
这里有许多好的答案,涵盖了许多重点,所以我只是添加一些我直接在上面没有看到的问题.也就是说,这个答案不应该被认为是综合的利弊,而是这里的其他答案的附录.
假设文件已经完全缓存1作为基线2,mmap
可能看起来非常像魔术:
mmap
只需要1个系统调用(可能)映射整个文件,之后不再需要系统调用.mmap
不需要从内核到用户空间的文件数据的副本.mmap
允许您访问文件"作为内存",包括使用您可以对内存执行的任何高级技巧来处理它,例如编译器自动向量化,SIMD内在函数,预取,优化的内存中解析例程,OpenMP等.在文件已经在缓存中的情况下,似乎无法击败:您只是直接访问内核页面缓存作为内存,它不会比这更快.
好吧,它可以.
mmap
vs的主要隐藏成本read(2)
(实际上是用于读取块的 OS级别系统调用)是,mmap
您需要为用户空间中的每个4K页面执行"一些工作",即使它可能被隐藏页面错误机制.
举个例子,一个典型的实现只mmap
需要整个文件就可以进行故障,所以100 GB/4K = 2500万个故障就可以读取100 GB的文件.现在,这些都是小错误,但是250亿页面错误仍然不会超级快.在最好的情况下,轻微故障的成本可能在100s纳米.
现在,您可以传递MAP_POPULATE
给mmap
它以在返回之前设置所有页面表,因此访问它时应该没有页面错误.现在,这有一个小问题,它也将整个文件读入RAM,如果你试图映射一个100GB的文件,这将会爆炸 - 但现在让我们忽略它3.内核需要按页面工作来设置这些页面表(显示为内核时间).这最终成为该mmap
方法的主要成本,并且它与文件大小成比例(即,随着文件大小的增长,它不会变得相对不那么重要)4.
最后,即使在用户空间访问中,这种映射也不是完全免费的(与不是源自基于文件的大型内存缓冲区相比mmap
) - 即使一旦设置了页表,每次访问新页面都会进行,从概念上讲,招致TLB未命中.由于mmap
文件意味着使用页面缓存及其4K页面,因此对于100GB文件,再次产生2500万次此成本.
现在,这些TLB未命中的实际成本在很大程度上取决于硬件的至少以下几个方面:(a)您拥有多少4K TLB内容以及其余的翻译缓存如何执行(b)硬件预取处理的程度如何使用TLB - 例如,可以预取触发页面遍历吗?(c)页面行走硬件的速度和平行程度.在现代高端x86英特尔处理器上,页面行走硬件通常非常强大:至少有2个并行页面步行器,页面遍历可以同时发生并继续执行,硬件预取可以触发页面遍历.因此,TLB 对流式读取负载的影响相当低 - 无论页面大小如何,此类负载通常都会执行相似的操作.但是,其他硬件通常要差得多!
该read()
系统调用,这是一般伏于"块读"类型提供例如,在C,C++等语言通话都有一个主要的缺点,每个人都充分认识到:
read()
调用N个字节必须将N个字节从内核复制到用户空间.另一方面,它避免了大部分成本 - 您不需要将2500万个4K页面映射到使用空间.您通常可以malloc
在用户空间中使用单个缓冲区小缓冲区,并为您的所有read
呼叫重复使用该缓冲区.在内核方面,4K页面或TLB未命中几乎没有问题,因为所有RAM通常使用一些非常大的页面(例如,x86上的1 GB页面)进行线性映射,因此页面缓存中的底层页面被覆盖在内核空间非常有效.
因此,基本上您可以进行以下比较,以确定单个读取大文件的速度更快:
这种mmap
方法隐含的额外每页工作是否比使用隐含的内核到用户空间复制文件内容的每字节工作成本更高read()
?
在许多系统中,它们实际上是近似平衡的.请注意,每个扩展都具有完全不同的硬件和OS堆栈属性.
特别是,在以下情况下,该mmap
方法变得相对更快
MAP_POPULATE
实现,可以在例如底层页面在物理内存中连续的情况下有效地处理大型映射.......在以下情况下,read()
方法变得相对较快:
read()
系统调用具有良好的复制性能.例如,copy_to_user
内核方面的良好性能.上述硬件因素在不同平台上变化很大,即使在同一系列中(例如,在x86代以内,特别是市场领域内),并且肯定跨越体系结构(例如,ARM vs x86与PPC).
OS因素也在不断变化,双方都有各种改进,导致一种方法或另一种方法的相对速度大幅上升.最近的清单包括:
mmap
没有MAP_POPULATE
.copy_to_user
方法arch/x86/lib/copy_user_64.S
,例如,REP MOVQ
在快速使用时,这确实有助于这种read()
情况.Spectre和Meltdown漏洞的缓解大大增加了系统调用的成本.在我测量的系统上,"无所事事"系统调用的成本(除了调用所做的任何实际工作之外,它是对系统调用的纯开销的估计)从典型的大约100 ns开始现代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倍或者.
小智 7
对不起Ben Collins丢失了他的滑动窗口mmap源代码.在Boost中很高兴.
是的,映射文件要快得多.您实际上是使用操作系统虚拟内存子系统来关联内存到磁盘,反之亦然.以这种方式思考:如果操作系统内核开发人员可以更快地实现它.因为这样做可以更快地完成所有事情:数据库,启动时间,程序加载时间等等.
滑动窗口方法确实不是那么困难,因为可以同时映射多个连续页面.因此,只要任何单个记录中的最大记录适合内存,记录的大小就无关紧要.重要的是管理簿记.
如果记录未在getpagesize()边界上开始,则映射必须从上一页开始.映射区域的长度从记录的第一个字节(如果需要向下舍入到最接近的getpagesize()的倍数)延伸到记录的最后一个字节(向上舍入到最接近的getpagesize()的倍数).处理完记录后,可以取消映射(),然后继续处理.
使用CreateFileMapping()和MapViewOfFile()(和GetSystemInfo()来获取SYSTEM_INFO.dwAllocationGranularity ---而不是SYSTEM_INFO.dwPageSize),这在Windows下也可以正常工作.
mmap 应该更快,但我不知道多少。这在很大程度上取决于您的代码。如果您使用 mmap,最好一次对整个文件进行 mmap,这将使您的生活更轻松。一个潜在的问题是,如果您的文件大于 4GB(或者实际上限制更低,通常为 2GB),您将需要 64 位架构。因此,如果您使用的是 32 位环境,您可能不想使用它。
话虽如此,可能有更好的方法来提高性能。你说输入文件被扫描了很多次,如果你可以一次读取它然后完成它,那可能会快得多。
归档时间: |
|
查看次数: |
72613 次 |
最近记录: |