Linux/Perl mmap 性能

10
我正在尝试使用mmap来优化处理大型数据集。数据集大小在千兆字节级别。想法是将整个文件映射到内存中,允许多个进程同时处理数据集(只读)。但是实际上并没有按照预期工作。
作为简单的测试,我仅仅使用perl的Sys::Mmap模块来进行文件映射(使用“mmap”子程序,我相信它直接映射到底层C函数),然后让该进程休眠。当执行此操作时,代码在从mmap调用返回之前花费了一分钟以上的时间,尽管这个测试没有对mmap映射的文件做任何操作,甚至没有读取。
我猜想,也许Linux需要在第一次 mmap 映射时读取整个文件,因此在第一个进程(处于休眠状态)中映射文件后,我在另一个进程中调用了一个简单的测试,试图读取文件的前几兆字节。
令人惊讶的是,第二个进程也花费了很长时间才从 mmap 调用中返回,大约与第一次 mmap 文件的时间相同。
我确保使用了 MAP_SHARED,并且第一次映射文件的进程仍然处于活动状态(即它没有终止,并且 mmap 没有被解除映射)。
我期望 mmap 文件将允许我给多个工作进程有效地随机访问大文件,但是如果每个 mmap 调用都需要先读取整个文件,那就有点困难了。我还没有测试使用长时间运行的进程来查看在第一次延迟后是否可以快速访问,但是我预计使用 MAP_SHARED 和另一个单独的进程应该足够了。
我的理解是 mmap 应该会立即返回,而 Linux 应该按需加载块,但是我看到的行为相反,表明它需要在每次调用 mmap 时读取整个文件。
你知道我做错了什么,或者我完全误解了 mmap 的工作原理吗?
9个回答

17

好的,找到问题了。正如猜测的那样,问题既不在Linux上也不在Perl上。我打开并访问文件的方法如下:

#!/usr/bin/perl
# Create 1 GB file if you do not have one:
# dd if=/dev/urandom of=test.bin bs=1048576 count=1000
use strict; use warnings;
use Sys::Mmap;

open (my $fh, "<test.bin")
    || die "open: $!";

my $t = time;
print STDERR "mmapping.. ";
mmap (my $mh, 0, PROT_READ, MAP_SHARED, $fh)
    || die "mmap: $!";
my $str = unpack ("A1024", substr ($mh, 0, 1024));
print STDERR " ", time-$t, " seconds\nsleeping..";

sleep (60*60);

如果您测试该代码,就不会像我原来的代码那样出现延迟,而在创建最小示例后(总是这样做,对吧!),原因突然变得很明显。
错误在于,在我的代码中,我将$mh标量视为句柄,这是轻量级的并且可以轻松移动(即按值传递)。结果,它实际上是一个GB长的字符串,绝对不是您想要移动而不创建显式引用(perl语言中的“指针”/句柄值)。因此,如果您需要将其存储在哈希或类似数据结构中,请确保存储\$mh,并在需要使用它时进行解引用,例如${$hash->{mh}},通常作为substr或类似函数的第一个参数。

9
如果您使用的是相对较新版本的Perl,则不应该使用Sys :: Mmap。您应该使用PerlIO的mmap层。请参考这里
您能否发布您正在使用的代码?

1
同意,PerlIO mmap层可能更可取,因为它还可以通过添加/删除mmap属性来允许相同的代码在有/无mmap的情况下运行。不管怎样,我找到了问题,发布了代码,问题解决了。 - Marius Kjeldahl
让问题解决,最多达到2GB。对于更大的文件,Perl仍然存在问题,请参阅我相关的其他回答。 - Marius Kjeldahl
PerlIO的mmap层是否适用于访问/dev/mem的读写块? - donaldh
@donaldh 我不知道,如果你能给我发一些能够做到这个的代码(用C或其他语言编写),我可以尝试用Perl复制它。 - Chas. Owens

3
在32位系统上,mmap()的地址空间相当有限(并且因操作系统而异)。如果您正在使用多GB文件并且仅在64位系统上进行测试,请注意这一点。(我本来想在评论中写这个,但我还没有足够的声望点数)

+1。在我看来,这似乎是一个有效的回答,解决了所提出的问题,因此感谢您没有将其发布为评论。 - Dave Sherohman
正如我在另一个回答中提到的那样,即使在64位系统上,处理更大的文件(> 2GB)仍然存在问题。不过你的回答是正确的。我所有的机器都已经使用64位了,甚至包括笔记本电脑,所以这对我不是问题。 - Marius Kjeldahl

1

提高性能的一种方法是使用'madvise(2)'。最简单的方法可能是通过Inline::C完成。'madvise'允许您告诉内核您的访问模式将是什么样子(例如,顺序,随机等)。


1
如果我可以推荐自己的模块:我建议使用File::Map而不是Sys::Mmap。它更容易使用,比Sys::Mmap更少崩溃。

这里有一个新的非常有用的功能建议,基于我对Perl在这个线程中的观察(内存映射文件仅能工作到2 GB); 如果用户映射的文件大于2 GB,则使用分段方法,并配合“自定义”读取函数,该函数会根据需要自动取消映射/映射。至少在2 GB Perl“错误”被修复之前应该这样做。 - Marius Kjeldahl

0

听起来确实令人惊讶。为什么不尝试使用纯C版本呢?

或者在不同的操作系统/Perl版本上尝试您的代码。


我已经查看了perl的操作系统接口,它直接或间接地调用C版本,但除非我弄清楚,否则我可能也会测试C版本。至于操作系统/ perl版本,我已在两个x86_64系统上进行了测试。一个是Ubuntu 8.04.2(linux 2.6.24-22,perl 5.8.8),另一个是Ubuntu 9.04(linux 2.6.28-13,perl 5.10.0)。表现相同。第二个系统是一台笔记本电脑,我可以明确确认从我的测试中调用mmap时涉及严重的磁盘io。 - Marius Kjeldahl

0
请参考 Wide Finder 获取有关使用 mmap 提高 Perl 性能的信息。但是有一个大陷阱。如果您的数据集位于经典硬盘上并且您将从多个进程中读取,您很容易遇到随机访问问题,导致 IO 速度下降到不可接受的水平(20~40 倍)。

我想要做的是通过设计实现多进程的随机访问,确保只有最常访问的文件部分始终在内存中。如果需要从多个进程进行随机访问并且文件很大,您会建议使用哪种模式? - Marius Kjeldahl
如果你真的需要对大文件进行随机访问,那么没有更好的解决方案。 - Hynek -Pichi- Vychodil
更好的解决方案是排队一堆读请求,限制短时间超时,然后按最佳顺序从磁盘读取块,以最小化寻道时间。我不确定是否有任何文件系统已经这样做了,我不认为ZFS会这样做。它在许多并发进程运行时效果最佳,例如Web服务器,或使用不同的IO API。 - Sam Watkins
@Sam Watkins:不错,你为IO调度器做出了贡献。你可以做得更好,但这需要大量的工作和技巧,比如禁用操作系统缓存、禁用操作系统预读等等。祝你在这个任务中好运。 - Hynek -Pichi- Vychodil

0

好的,这里有一个更新。在Perl中使用Sys::Mmap或PerlIO的“:mmap”属性都可以正常工作,但仅限于2 GB文件(32位限制)。一旦文件超过2 GB,就会出现以下问题:

使用Sys::Mmap和substr访问文件时,似乎substr仅接受32位int作为位置参数,即使在perl支持64位的系统上也是如此。至少有一个错误报告:

#62646: Maximum string length with substr

使用open(my $fh, "<:mmap", "bigfile.bin"),一旦文件大于2 GB,似乎perl要么挂起/要么坚持在第一次读取时读取整个文件(不确定哪个,我从未运行足够长时间以查看是否完成),导致性能极慢。

我没有找到任何解决方法,目前我只能使用较慢的非mmap操作来处理这些文件。除非我找到解决方法,否则我可能必须使用C或其他更高级别的语言来实现处理,以更好地支持mmap巨大文件。


1
尝试直接使用Sys :: Mmap中的mmap来创建标量中的滑动窗口。 - Chas. Owens
谢谢,那肯定是一个解决方法。这将需要跟踪文件指针并在必要时进行映射/取消映射,这可能会影响性能。但它可能仍然比直接文件IO快。 - Marius Kjeldahl
进行了一些基准测试,确认使用2 GB的段大小动态映射/取消映射,并假设段切换相当不频繁时,使用mmap与取消映射/映射比在3 GB文件上直接使用文件IO快30-40%。在2 GB文件上差异较小,但我怀疑这是由于我的笔记本电脑在随机访问期间缓存了大部分文件。所以至少我有一个可行的解决方案,尽管不如我希望的那样干净。目前不需要进一步优化。 - Marius Kjeldahl

0

如果您的文件访问不是均匀分布的,最好使用随机访问来证明完整的mmap。如果您的使用情况不是均匀分布的,最好使用seek,读取到新malloc的区域并处理,释放,清洗和重复。并且使用4k的倍数块进行工作,例如64k左右。

我曾经对很多字符串模式匹配算法进行了基准测试。将整个文件映射到内存中是缓慢而无意义的。读取到静态的32k缓冲区更好,但仍然不是特别好。读取到新malloc的块中,处理它,然后让它消失,可以让内核在幕后发挥奇迹。速度差异是巨大的,但是模式匹配在复杂性方面非常快,因此必须更加注重处理效率,可能比通常需要的更多。


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