在Perl中更有效地处理AoA的笛卡尔积

时间:2014-03-28 16:31:27

标签: r performance perl cartesian-product

我正在计算具有相同值的组中两个项目的概率值(与生日问题类似的情况http://en.wikipedia.org/wiki/Birthday_problem)。

要做到这一点,我有24组三个值。组中的每个项目将具有来自24组中的每一组中的3个值。

我需要做的计算是获得这些值的所有可能迭代的乘积平方和。

考虑到必然的迭代性,这种迭代显然是非常密集的。

来自SE的输入我现在已经:

#!perl;
use List::Util qw(reduce);
use Set::CrossProduct;

my @array = ( ## AoA containing values for caluculation, cut-down to allow benchmarking
#   [0.33, 0.33, 0.33],  x11 more in full set
    [0.33, 0.33, 0.33],
    [0.33, 0.33, 0.33],
    [0.33, 0.33, 0.33],
    [0.33, 0.33, 0.33],
    [0.33, 0.33, 0.33],
    [0.33, 0.33, 0.33],
    [0.33, 0.33, 0.33],
    [0.33, 0.33, 0.33],
    [0.33, 0.33, 0.33],
    [0.33, 0.33, 0.33],
    [0.33, 0.33, 0.33],
    [0.33, 0.33, 0.33],
    [0.33, 0.33, 0.33]
);

$val = 0;
my $iterator = Set::CrossProduct->new(\@array);
while (my $tuple = $iterator->get) {
    $freq = reduce { $a * $b } @$tuple;
    $val += ($freq*$freq);
}

$toprint=sprintf("%.50e", $val);
print $toprint;

基于上面代码中13组子集的快速基准测试,我估计在我的PC上运行完整的24组需要大约45天。是否有关于如何改进性能的建议?我不是在寻找奇迹,我很乐意在一周内完成它......

我没有在Perl上投入情感,所以如果有显着的性能优势,可以尝试转换到另一种语言。

提前感谢任何建议。

编辑:添加了R标签,因为这可能是我能够实施解决方案的第二好标签。

1 个答案:

答案 0 :(得分:3)

这类问题是我的一杯茶。以下是我的想法:


让我们退后一步

这里的关键目标是减少评估结果所需​​的时间。您需要执行3 ^ 24 = 282亿个评估,这是无法避免的。但是,有一些技巧可以用来解决问题的更轻松的工作(评论也提到其中一些):

  1. 平行努力以缩短所需时间
  2. 避免重复计算

  3. 并行计算

    分而治之

    解锁并行化的关键(如前所述)是将工作分成更小的部分。在这个问题的上下文中,元组需要分成更易于管理的块。

    如果我有一个四核处理器,我可能想把元组分成四个篮子:

    my ( @baskets, $iter );
    push @{ $baskets[ $iter++ % 4 ] }, $_ for $iterator->combinations;
    

    这种功能非常容易归入一个子目录:

    sub segment {
    
      my $num_segments = shift;
      my ( @baskets, $iter );
    
      push @{ $baskets[ $iter++ % $num_segments ] }, $_ for @_;
      return @baskets;
    }
    
    my @jobs = segment( 4, $iterator->combinations );
    

    并行发布

    这里的线程使用应该足够,因为每元组计算是轻量级的(有关如何在Perl中使用线程的更多信息,请参考perldoc perlthrtut):

    use threads;                                            # imports threads module
    
    sub work {                                              # What each thread will run
    
      my @tuples = @_;
    
      my $sum;
      for my $tuple ( @tuples ) {
    
        my $freq = 1;
        $freq *= $_ for @$tuple;
        $sum += $freq * $freq;
      }
    
      return $sum;
    }
    
    my @threads = map threads->new( \&work, @$_ ), @jobs;  # Create and launch threads
                                                           # with different tuple sets
    
    my $grand_total;
    $grand_total += $_->join for @threads;                 # Accumulate sub-totals
    

    用1石头杀死 n 的鸟类(乘以 n

    免责声明:随着离散概率数量的增加,此解决方案的有效性也会增加。要判断这个提案是否会真正缩短获得结果的时间并不容易。

    假设2 d.p.,所有元组中只能有100个不同的值(我想这就是生日问题发挥作用的地方)。鉴于每个元组中有24个概率,我想两个元组产生相同频率的可能性很高(统计学家可以证实这个假设)。这可以用一个简单的例子来证明,其中我将概率的数量限制在3:

    [ 0.33, 0.45, 0.22 ], # Tuple A
    .
    .
    .
    [ 0.45, 0.22, 0.33 ], # Tuple B
    

    此处,元组A和B将返回$freq的相同值。如果我们计算这个$freq值出现的次数,可以简单地计算$freq一次并将其乘以“重复”元组的数量(从而用一块石头杀死许多元组)。 / p>

    这将涉及检测重复次数:

    my %seen;
    for my $tuple ( $iterator->combinations ) {
    
        my @sorted = sort @$tuple;
        my $tuple_as_string = "@sorted";
    
        $seen{$tuple_as_string}{count}++;
    
        next unless exists $seen{$tuple_as_string}{freq};
    
        my $freq = 1;
        $freq *= $_ for @$tuple;
    
        $seen{$tuple_as_string}{freq} = $freq;
    }
    
    
    my $grand_total;
    for my $unique ( keys %seen ) {
    
        my $count = $seen{$unique}{count};
        my $freq = $seen{$unique}{freq};
        $grand_total += $count * $freq * $freq;
    }
    

    如果您希望将此想法与并行化相结合,我建议在继续并行化操作之前先识别“唯一”元组。