如何提高Perl比较性能

时间:2013-07-07 11:16:10

标签: perl

我有一个大约50,000个用户的数组引用。我想通过所有这些用户并将每个用户与其他所有用户进行比较,以便建立一个加权的匹配列表(如果名称是完全匹配,则值x,部分匹配值等)。

在完成列表并完成所有检查之后,我想要获得10个最高加权匹配。以下是我正在做的帮助解释的一个例子:

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

my $users = [];
$users->[0]{'Name'} = 'xxx';
$users->[0]{'Address'} = 'yyyy';
$users->[0]{'Phone'} = 'xxx';
$users->[1]{'Name'} = 'xxx';
$users->[1]{'Address'} = 'yyyy';
$users->[1]{'Phone'} = 'xxx';
$users->[2]{'Name'} = 'xxx';
$users->[3]{'Address'} = 'yyyy';
$users->[4]{'Phone'} = 'xxx';
foreach my $user_to_check (@$users) {
    my $matched_users = [];
    foreach my $user (@$users) {            
        $user_to_check->{'Weight'} = 0;
        if (lc($user_to_check->{'Name'}) eq lc($user->{'Name'})) {
            $user_to_check->{'Weight'} = ($user_to_check->{'Weight'} + 10);
        } elsif ((length($user_to_check->{'Name'}) > 2) && (length($user->{'Name'}) > 2) && ($user_to_check->{'Name'} =~ /\Q$user->{'Name'}\E/i)) {
            $user_to_check->{'Weight'} = ($user_to_check->{'Weight'} + 5);
        }
        if (lc($user_to_check->{'Address'}) eq lc($user->{'Address'})) {
            .....
        }
        if ($user_to_check->{'Weight'} > 0) {
            # We have matches, add to matched users
            push (@$matched_users,$user);
        }
    }
   # Now we want to get just the top 10 highest matching users
   foreach my $m_user (sort { $b->{'Weight'} <=> $a->{'Weight'} } @$matched_users ) {
    last if $counter == 10;
       .... # Do stuff with the 10 we want
    }         
}                            

问题是,它太慢了。运行需要一天多的时间(我已经在多台机器上试过了)。我知道“排序”是一个杀手,但我也尝试将结果插入到tmp mysql表中,然后在最后而不是进行Perl排序,我只是通过选择做了一个订单,但时间的差异非常大次要的。

由于我正在浏览现有的数据结构并进行比较,因此我不确定我能做些什么(如果有的话)来加快速度。我很感激任何建议。

1 个答案:

答案 0 :(得分:15)

O(N²)

您将@$users中的每个元素与其中的每个元素进行比较。那是5E4²= 2.5E9的比较。例如,您不需要将元素与自身进行比较。您也不需要将元素与已比较的元素进行比较。即在这个比较表中

  X Y Z
X - + +
Y - - +
Z - - -

只需将三个比较与每个元素进行比较。您正在进行的九项比较是66%的必要(渐近:50%不需要)。

您可以通过循环索引来实现此目的:

for my $i (0 .. $#$users) {
  my $userA = $users->[$i];
  for my $j ($i+1 .. $#$users) {
    my $userB = $users->[$j];
    ...;
  }
}

但这意味着在匹配时,您必须增加两个匹配用户的权重。

做一次,而不是100,000次

您将每个用户的名称小写1E5次。这是1E5 - 1倍!只需为每个元素执行一次,可能是在数据输入时。

作为旁注,你不应该执行小套,你应该做案例折叠。这可以通过fc功能至少获得v16。当你有非英语数据时,只需小写就会出错。

use feature 'fc'; # needs v16
$user->[NAME] = fc $name;

use Unicode::CaseFold;
$user->[NAME] = fc $name;

当哈希值不够快时

哈希很快,因为查找需要不变的时间。但是单个哈希查找比数组访问更昂贵。由于您只有一组小的预定义字段,因此您可以使用以下技巧来使用类似哈希的数组:

使用映射到索引的字段名称声明一些常量,例如

use constant {
  WEIGHT => 0,
  NAME => 1,
  ADDRESS => 2,
  ...;
};

然后将数据放入数组:

$users->[0][NAME] = $name; ...;

您可以访问

等字段
$userA->[WEIGHT] += 10;

虽然这看起来像一个哈希,但这实际上是一种安全的方法,只能以最小的开销访问数组的某些字段。

正则表达式很慢

嗯,它们非常快,但有一种更好的方法来确定字符串是否是另一个字符串的子字符串:使用index。即。

$user_to_check->{'Name'} =~ /\Q$user->{'Name'}\E/i

可以写成

(-1 != index $user_to_check->{Name}, $user->{Name})

假设两者都已经小写案例折叠。

替代实施

修改:您对问题的修改似乎无效。这假设您试图找到一些全局相似性,而不是为每个用户获得一组好的匹配

实现这些想法会使你的循环看起来像

for my $i (0 .. $#$users) {
  my $userA = $users->[$i];
  for my $j ($i+1 .. $#$users) {
    my $userB = $users->[$j];
    if ($userA->[NAME] eq $userB->[NAME]) {
        $userA->[WEIGHT] += 10;
        $userB->[WEIGHT] += 10;
    } elsif ((length($userA->[NAME]) > 2) && (length($userB->[NAME]) > 2))
        $userA->[WEIGHT] += 5 if -1 != index $userA->[NAME], $userB->[NAME];
        $userB->[WEIGHT] += 5 if -1 != index $userB->[NAME], $userA->[NAME];
    }
    if ($userA->[ADDRESS] eq $userB->[ADDRESS]) {
        ..... # More checks
    }
  }
}
my (@top_ten) = (sort { $b->[WEIGHT] <=> $a->[WEIGHT] } @$users)[0 .. 9];

分而治之

您展示的任务是高度可并行化的。如果你有内存,在这里使用线程很容易:

my $top10 = Thread::Queue->new;
my $users = ...; # each thread gets a copy of this data

my @threads = map threads->create(\&worker, $_), [0, int($#$users/2)], [int($#$users/2)+1, $#users];

# process output from the threads
while (defined(my $ret = $top10->dequeue)) {
  my ($user, @top10) = @$ret;
  ...;
}

$_->join for @threads;

sub worker {
  my ($from, $to) = @_;
  for my $i ($from .. $to) {
    my $userA = $users->[$i];
    for $userB (@$users) {
      ...;
    }
    my @top10 = ...;
    $top10->enqueue([ $userA, @top10 ]); # yield data to the main thread
  }
}

您应该通过队列返回输出(如此处所示),但在线程内尽可能多地处理。通过更高级的工作负载分区,应该产生与可用处理器一样多的线程。

但是如果任何类型的流水线操作,过滤或缓存都可以减少嵌套循环中所需的迭代次数,那么你应该进行这样的优化(想想map-reduce风格的编程)。

编辑:通过重复数据删除的哈希来优雅地降低复杂性

我们基本上正在做的是计算我们的记录匹配的好的矩阵,例如

  X Y Z
X 9 4 5
Y 3 9 2
Z 5 2 9

如果我们假设 X类似于Y 暗示 Y类似于X ,那么矩阵是对称的,我们只需要它的一半:

  X Y Z
X \ 4 5
Y   \ 2
Z     \

这样的矩阵相当于加权的无向图:

4  X  5   |  X – Y: 4
  / \     |  X – Z: 5
 Y---Z    |  Y – Z: 2
   2      |

因此,我们可以优雅地将其表示为哈希的散列:

my %graph;
$graph{X}{Y} = 4;
$graph{X}{Z} = 5;
$graph{Y}{Z} = 2;

但是,这种散列结构意味着一个方向(从节点X到节点Y)。为了更容易查询数据,我们也可以包括另一个方向(由于哈希的实现,这不会导致大量内存增加)。

$graph{$x}{$y} = $graph{$y}{$x} += 2;

因为每个节点现在只连接到那些类似的节点,所以我们不必对50,000条记录进行排序。对于第100条记录,我们可以获得十个最相似的节点,如

my $node = 100;
my @top10 = (sort { $graph{$node}{$b} <=> $graph{$node}{$a} } keys %{ $graph{$node} })[0 .. 9];

这会将实现更改为

my %graph;

# build the graph, using the array indices as node ID
for my $i (0 .. $#$users) {
  my $userA = $users->[$i];
  for my $j ($i+1 .. $#$users) {
    my $userB = $users->[$j];
    if ($userA->[NAME] eq $userB->[NAME]) {
        $graph{$j}{$i} = $graph{$i}{$j} += 10;
    } elsif ((length($userA->[NAME]) > 2) && (length($userB->[NAME]) > 2))
        $graph{$j}{$i} = $graph{$i}{$j} += 5
          if -1 != index $userA->[NAME], $userB->[NAME]
          or -1 != index $userB->[NAME], $userA->[NAME];
    }
    if ($userA->[ADDRESS] eq $userB->[ADDRESS]) {
        ..... # More checks
    }
  }
}

# the graph is now fully populated.

# do somethething with each top10
while (my ($node_id, $similar) = each %graph) {
  my @most_similar_ids = (sort { $similar->{$b} <=> $similar->{$a} } keys %$similar)[0 .. 9];
  my ($user, @top10) = @$users[ $node_id, @most_similar_ids ];
  ...;
}

以这种方式构建图形应该花费原始迭代的一半时间,并且如果每个节点的平均边缘数足够低,那么通过相似的节点应该会快得多。

并行化这有点困难,因为每个线程生成的图必须在查询数据之前进行组合。为此,每个线程最好执行上述代码,但迭代边界作为参数给出,并且只生成一个边。这对边缘将在组合阶段完成:

THREAD A [0 .. 2/3]   partial
                   \  graph
                    =====> COMBINE -> full graph -> QUERY
                   /  partial
THREAD B [2/3 .. 1]   graph

# note bounds recognizing the triangular distribution of workload

但是,如果给定节点的节点非常少,这只会很有用,因为组合很昂贵。