Parallel::ForkManager使得子例程变慢1000倍

3

我有一个子程序, 我已经尽我所能进行串行优化, 大概像这样:

sub overlap {

    my $hash_reference = shift;   # pass the hash to the subroutine
    my %h = %{ $hash_reference }; # refer to the hash as %h
    my $standard = shift;         # this is the key that will be compared against
    my $compared = shift;         # this is the key being compared
    my $start_index = 0;          # this will continually be increased
                                  # to save computation time

    # I want to parallelize here

    foreach my $s ( 0 .. scalar @{ $h{$standard}{end} }-1 ) {
        foreach my $c ( $start_index .. scalar @{ $h{$compared}{end} }-1 ) {
            ... # abbreviated for minimal working example
        }
    }

    return ($standard_indices_met_in_compared, \@overlay);
}

这是一个慢速子程序。我大约需要花费12-14分钟运行数千次,但是反复运行会浪费时间。

我经常使用Parallel::ForkManager来进行系统进程处理,但在这里效果不佳。

Parallel::ForkManager的实现如下:

use Parallel::ForkManager qw();
my $manager = new Parallel::ForkManager(2);
foreach my $s ( 0 .. scalar @{ $h{$standard}{end} }-1 ) {

    foreach my $c ( $start_index .. scalar @{ $h{$compared}{end} }-1 ) {
        $manager->start and next;
        ... # abbreviated for minimal working example
    }

    $manager->finish;
}

$manager->wait_all_children;      # necessary after all lists

我看了一些帖子,但没有看到如何在这里应用。

我看过 Perl 多线程和 foreach,以及 Perl 线程文档和其他许多来源,但我不知道如何将以前的做法应用到这种情况。我看到的所有东西都只适用于系统命令。

我想写入一个共享数组和标量,没有系统命令。如果我漏掉了什么,请告诉我。

如何在子例程内并行化此 foreach 循环?


2
我能想到的一件事是你把整个 P::FM 设置放在循环内部了吗?那样会很糟糕。简而言之,将对象放在循环外面,只在子代码内部使用。你只需要一个对象来完成整个操作。 - zdim
5
我不知道如何将以前做过的东西应用到这个案例中”,使用线程来做与P::FM相同的事情很简单,但是你说P::FM对你没用,所以这可能对你也没用。我不想浪费时间去猜你想要什么,请具体说明你在使用P::FM时遇到了什么问题。“这里不起作用”远远不能作为一个足够的问题描述。 - ikegami
1
你并没有提及任何有关速度的信息,所以不要责备我没有假设那是问题所在。而且,那仍然是几乎毫无用处的信息。请展示使其变慢的原因!它每个进程执行的操作太少了吗?它使用的内存太多了吗?你还没有向我们展示问题所在!!!你需要再思考一下。请不要假装你正在尝试并行化代码,因为你已经做到了。找出你要解决的问题,并给出一些信息,这将帮助我们解决这个问题。请通过修复你的问题来包含这些信息。 - ikegami
4
在循环内部开始并在外部结束会使每个分叉的进程完成该循环,这绝对不是您想要的。 - ysth
3
了解您要并行化的具体任务非常重要,因为并不是所有的任务都能在资源实际上有互相依赖时更快地完成。运行更多进程或线程总会带来一定的开销。对于CPU密集型任务,最好将CPU核心数与工作进程数匹配。如果您尝试让1000个进程更新数据库中的同一张表,可能会遇到争用问题。如果您尝试在内存中读取1000个文件,则可能会遇到IO问题。等等。并没有万能的解决方案来解决所有问题。 - xxfelixxx
显示剩余9条评论
2个回答

4

你真的只想使用最多两个进程来并行处理吗?如果是这样,那么这可能是导致感觉速度慢的原因。

并行化总会存在一定的开销。如果你在10个进程上并行化,不能保证10倍的加速。

我建议您将最大进程数增加到更合理的数量,然后再试一次。如果这没有帮助,可能是由于以下原因:

  • 硬件限制
  • 你尝试并行化的循环中有些东西强制顺序执行(例如写入同一个文件、数据库表、更新信号量或共享变量等)

2
IO是一个经典的东西,不太容易并行化。磁盘只能以一定的速度旋转,而通过添加并发、乱序读取的争用,会降低吞吐量。 - Sobrique
嗨,Zaid,感谢你周到的回复。我已经在P::FM中尝试了2、4和8个CPU,但所有的东西都比串行运行慢得多。该子例程在foreach语句中工作,我正在遍历一个哈希数组。Sobrique- 该子例程不使用任何I/O,只使用一个哈希数组。所有变量都必须共享。也许线程是正确的方法,我会研究一下。 - con
Zaid,所有进程从同一个数组中读取,并写入同一个变量。也许这个过程不容易并行化,你的观点可能是正确的。 - con
Zaid的回答中最关键的一点是:“你尝试并行化的循环中可能存在某些强制顺序执行的因素(例如写入同一文件、数据库表、更新信号量、共享变量等)”。 - con

1
一旦我们开始看到Parallel::ForkManager部分,我想要指出所显示的直接错误,已经被ysth在评论中指出。
为了更清晰地表示循环,并且有一个更有意义的限制,您可以这样做。
use Parallel::ForkManager;
my $manager = Parallel::ForkManager->new(8);

foreach my $s ( ... )
{    
    foreach my $c ( ... ) 
    {
        $manager->start and next;    # 
        # code                       # WRONG
    }                                # Module: Can't fork inside child
    $manager->finish;                #
}
$manager->wait_all_children;

让我们看看这个尝试做什么。
在内部循环中创建了一个子进程,但它会在外部退出,这意味着它会运行整个循环。因此,每个子进程也将执行创建新子进程的代码行,以及父进程。这将是一场灾难,导致大量子进程级联并且工作分配错误。
但是该模块不允许这种情况发生,并会抛出错误。你的实际代码与显示的示例不同吗?
现在考虑。
foreach my $s ( ... ) 
{    
    $manager->start and next;     # child forked

    foreach my $c ( ... ) 
    {                             # Whole inner loop
        # code                    # run by one child
    }                             # for one value of $s

    $manager->finish;             # child exits
}    

一个分叉在内部循环之外发生,子进程继续使用当前的$s值运行整个循环。父进程跳到外部循环的下一个迭代并分叉另一个子进程,该子进程为$s的下一个值运行内部循环。每个子进程都为后续的$s值运行整个内部循环。因此,外部循环的迭代是并行执行的。
这就是你想要的。所以改变你的代码以实现这一点,并看看效果如何。
重申一下,不是所有的代码都能从并行运行中受益。有些代码根本无法正确地并行运行,而有些代码可能会遭受明显的性能下降。

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