Perl进程之间共享只读内存

9
我想让我的Perl程序使用多个核心。它会逐步读取查询输入并将其与从文件加载到内存中的只读数据结构的块进行比较,用于每次运行。该数据结构通常为几千兆字节,是一组小C例程中使用的打包字符串。当进程被分叉时,所有内容都会被复制,这在多核机器上很快会使RAM耗尽。我尝试了几个非标准模块,但都导致速度变慢和/或RAM溢出。我认为,对于只读数据,Perl不需要坚持制作副本。其他语言可以做到。有人有什么想法吗?

1
这个讨论看起来很有趣:https://dev59.com/0mkw5IYBdhLWcg3wqMU0。特别是,有人建议将一些RAM挂载为硬盘,然后从那里使用文件I/O。这将解决您的问题。问题是:这值得吗? - dan1111
谢谢,使用文件是一种选择。但我认为孩子们会让磁盘旋转得如此之快,以至于多核速度比单核加上所有内存还要慢。我这么说是因为我有一个早期版本的程序使用了文件,速度慢了20倍。其中一个模块使用套接字来使内存“共享”,但这也会大大减慢速度。 - Niels Larsen
请注意,dan1111并不建议您使用实际文件。磁盘旋转对于这个解决方案没有影响。 - darch
啊,我看错了。但是创建ramfs是特定于操作系统的,而且我也希望程序能够以普通用户身份运行。我可能会尝试使用Sys::Mmap。 - Niels Larsen
“吹 RAM”是什么意思?我在谷歌上找不到与这个短语相关的任何内容。 - Starfish
@Starfish 这只是一个短语,意思是“程序消耗的内存超过系统可用内存”。通常情况下,Perl 会在这种情况下终止并显示“内存不足”的消息。 - amon
3个回答

2

Fork通常不会在修改之前复制内存(搜索copy on write或COW)。您确定您正在正确地测量内存使用情况吗?请从free的before/after值中减去,而不是使用top。

编辑-示例脚本

尝试使用以下设置运行以下内容: ./fork_mem_usage 5 10000 ./fork_mem_usage 25 10000 ./fork_mem_usage 5 100000 ./fork_mem_usage 25 100000

如果第一个增加比后续的增加更大,则fork正在使用写时复制。它几乎肯定是(当然除了Windows)。

#!/usr/bin/perl
use strict;
use warnings;

my $num_kids  = shift @ARGV;
my $arr_size  = shift @ARGV;
print "$num_kids x $arr_size\n";

my @big_array = ('abcdefg') x $arr_size;
die "Array wrong length" unless ($arr_size == @big_array);

print_mem_usage('Start');

for my $i (1..$num_kids) {
    my $pid = fork();
    if ($pid) {
        if ($i % 5 == 0) {
            print_mem_usage($i);
        }
    }
    else {
        sleep(5);
        exit;
    }
}

print_mem_usage('End');
exit;

sub print_mem_usage {
    my $msg = shift;
    print "$msg: ";
    system q(free -m | grep buffers/cache | awk '{print $3}');
}

是的,我遇到了“内存不足”的错误,副本已经创建。欢迎提出建议如何避免这些问题。我尝试了我能找到的所有方法。 - Niels Larsen
感谢您的详细回复。执行命令"./fork_mem_usage 25 100000",输出结果为:25 x 100000 开始:320 5秒后:322 10秒后:322 15秒后:322 20秒后:323 25秒后:323 结束:323 - Niels Larsen
所以第一个使用了约2MB,其余每个都少于1MB。无论你的问题是什么,它都不是fork复制只读内存。 - Richard Huxton
2
确实,你说得对。在我的旧联想笔记本电脑上,总共只有2GB的内存,可以启动任意数量的600MB子进程。我将你的测试更改为字符串哈希值,这更类似于我的数据结构,并在子进程中取随机子字符串。我很感激你把它放在我的鼻子底下,我肯定测试了普通的fork,但一定犯了错误。在我的墓碑上,我会写上“从未正确检查的人”。 - Niels Larsen

0

编辑和总结:

我对threads::shared是一个选项的想法非常错误。在线程创建时,即使是共享数据结构也会被复制。这确实很糟糕,因此我可以总结说,Perl完全无法进行内存密集型计算。


当一个进程fork时,内核会复制整个进程。驻留在RAM中的所有内容都会被复制。没有任何语言可以绕过这一点。但是,您可以尝试使用内存映射,或者您可以使用线程。
Perl线程是一个fork仿真器,但是您可以声明变量在线程之间共享:
use threads;
use threads::shared;

my $sharedVariable :shared = 0;

my @worker;

for my $i (1 .. 6) {
   push @worker, threads->create(\&worker_sub);
}

$_->join() foreach @worker;

sub worker_sub {
   sleep rand 5;
   print $sharedVariable, "\n";
}

如果在一个线程中更新了$sharedVariable,则更改也会传播到其他线程。如果您将打印语句替换为其他操作,就可以看到这一点。
print threads->tid, "-->", ++$sharedVariable, "\n";

谢谢..是的,我尝试了forks模块,据说它比线程更节省RAM,但是它增加了速度,并且(通过top)看起来内存被复制了。其他语言确实提供了零拷贝的方式,例如C++ Boost库。我尝试了Parallel::ForkManager、IPC::Shareable和IPC::ShareLite,但它们似乎都会复制。我可以将数据结构留在文件中,但是子进程将不断寻找,我认为程序也不会更快。除了使用另一种语言或新逻辑外,我不知道该怎么办了。 - Niels Larsen
@NielsLarsen forks 模块模拟了旧版 Perl 的线程,因为它实际上与 threads 具有相同的 API。(您使用的是哪个 Perl 版本?)ForkManager 使用一个带有序列化数据结构的文件进行数据共享 - 这不是您想要的。Shareable 在内存中执行相同的操作,因此全局只有一个序列化数据结构和每个进程一个反序列化数据结构。threads::shared 真的应该解决您的问题。整个问题的一部分是 Perl 不像 C 一样在原始内存上运行,而是为您管理内存,并具有垃圾回收的好处。 - amon
我在 Linux Mint Debian 上安装了 Perl 5.14.2,并使用 -Dusethreads 编译了它。谢谢,我将尝试使用 threads::shared,之前我跳过了它,因为我读到它非常低效。但我会尝试一下,然后发布结果。你是我的最后希望.. - Niels Larsen
@darch:好的,既然我已经尝试了所有能找到的方法,那么结论是什么呢?Perl不能做共享内存,我应该使用另一种语言吗? - Niels Larsen
1
@dan1111 只是为了澄清,Perl 线程 可以 有效地利用多个核心;它们是真正的操作系统级线程。只是它们有很多其他致命问题,使它们难以使用。 - hobbs
显示剩余8条评论

0

您可以使用Cache::FastMmap来存储共享数据。我听说有人将其用于IPC而不是缓存,这个缓存是在进程之间共享的。其中很大一部分是用C语言编写的。不要忘记在初始化时添加'raw_values=1'。可以压缩缓存中的值,因此如果您有足够的CPU和可压缩的数据,则可以节省大量内存。

它非常快,以下是一些基准测试:http://cpan.robm.fastmail.fm/cache_perf.html

由于Cache::FastMmap将一个共享文件映射到进程的内存空间中,这可能会使每个进程看起来相当大,即使它只是mmap'd内存,该内存在所有使用缓存的进程之间共享,并且甚至可能被交换出去,如果缓存使用率较低。

然而,操作系统会认为您的进程相当大,这可能意味着您之前设置的一些BSD::Resource或'ulimits'已经不再合理,因此请注意。


是的,我也遇到了Cache::FastMmap。但那是用于存储许多键/值对,其中值不能大于内存页...我的数据是一些无法分割的长数组。对于键/值存储,我将使用Kyoto Cabinet,它也是内存映射且更有效率。这就留下了逻辑重新思考或另一种语言,除非有一些专家出现并说出不同的意见。 - Niels Larsen

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