Perl Goatse的“秘密操作员”是否有效率?

时间:2010-10-21 20:45:45

标签: performance perl operators idioms goatse

Perl中的“goatse运算符”或=()=习语导致在列表上下文中计算表达式。

一个例子是:

my $str = "5 and 4 and a 3 and 2 1 BLAST OFF!!!";
my $count =()= $str =~ /\d/g; # 5 matches...
print "There are $count numbers in your countdown...\n\n";

当我解释使用时,会发生这种情况:

  1. $str =~ /\d/g匹配所有数字。 g开关和列表上下文生成这些匹配的列表。让它成为“List Producer”示例,在Perl中可以有很多东西。
  2. =()=导致对空列表的分配,因此所有实际匹配都会复制到空列表中。
  3. 标量上下文中的赋值给2中生成的列表的$ count给出了列表的计数或结果。
  4. 标量赋值后,空列表=()=的引用计数变为零。然后Perl删除列表元素的副本。
  5. 有关效率的问题是:

    1. 我在解析这个问题时错了吗?
    2. 如果您有一些List Producer,而您感兴趣的只是计数,那么有更有效的方法吗?
    3. 这个简单的列表很有用,但是如果列表是成千上万的匹配怎么办?使用此方法,您将生成每个匹配的完整副本,然后将其删除以计算它们。

3 个答案:

答案 0 :(得分:23)

Perl 5对复制列表非常聪明。它只复制左侧的项目。它的工作原理是因为标量上下文中的列表赋值会产生右侧的项目数。因此,正则表达式将创建n项,但它们不会被复制和丢弃,只是被丢弃。您可以在下面的基准测试中看到额外副本在幼稚情况下的差异。

至于效率,迭代解决方案通常更容易在内存和CPU使用上,但这必须与山羊秘密运营商的简洁性进行权衡。以下是对各种解决方案进行基准测试的结果:

naive: 10
iterative: 10
goatse: 10

for 0 items:
               Rate iterative    goatse     naive
iterative 4365983/s        --       -7%      -12%
goatse    4711803/s        8%        --       -5%
naive     4962920/s       14%        5%        --

for 1 items:
               Rate     naive    goatse iterative
naive      749594/s        --      -32%      -69%
goatse    1103081/s       47%        --      -55%
iterative 2457599/s      228%      123%        --

for 10 items:
              Rate     naive    goatse iterative
naive      85418/s        --      -33%      -82%
goatse    127999/s       50%        --      -74%
iterative 486652/s      470%      280%        --

for 100 items:
             Rate     naive    goatse iterative
naive      9309/s        --      -31%      -83%
goatse    13524/s       45%        --      -76%
iterative 55854/s      500%      313%        --

for 1000 items:
            Rate     naive    goatse iterative
naive     1018/s        --      -31%      -82%
goatse    1478/s       45%        --      -75%
iterative 5802/s      470%      293%        --

for 10000 items:
           Rate     naive    goatse iterative
naive     101/s        --      -31%      -82%
goatse    146/s       45%        --      -75%
iterative 575/s      470%      293%        --

以下是生成它的代码:

#!/usr/bin/perl

use strict;
use warnings;

use Benchmark;

my $s = "a" x 10;

my %subs = (
    naive => sub {
        my @matches = $s =~ /a/g;
        return scalar @matches;
    },
    goatse => sub {
        my $count =()= $s =~ /a/g;
        return $count;
    },
    iterative => sub {
        my $count = 0;
        $count++ while $s =~ /a/g;
        return $count;
    },
);

for my $sub (keys %subs) {
    print "$sub: @{[$subs{$sub}()]}\n";
}

for my $n (0, 1, 10, 100, 1_000, 10_000) {
    $s = "a" x $n;
    print "\nfor $n items:\n";
    Benchmark::cmpthese -1, \%subs;
}

答案 1 :(得分:13)

在您的特定示例中,基准测试非常有用:

my $str = "5 and 4 and a 3 and 2 1 BLAST OFF!!!";

use Benchmark 'cmpthese';

cmpthese -2 => {
    goatse => sub {
        my $count =()= $str =~ /\d/g;
        $count == 5 or die
    },
    while => sub {
        my $count; 
        $count++ while $str =~ /\d/g;
        $count == 5 or die
    },
};

返回:

           Rate goatse  while
goatse 285288/s     --   -57%
while  661659/s   132%     --

列表上下文中的$str =~ /\d/g正在捕获匹配的子字符串,即使它不是必需的。 while示例在标量(布尔)上下文中具有正则表达式,因此正则表达式引擎只需返回true或false,而不是实际匹配。

一般来说,如果你有一个列表生成函数并且只关心项目数,那么写一个简短的count函数会更快:

sub make_list {map {$_**2} 0 .. 1000}

sub count {scalar @_}

use Benchmark 'cmpthese';

cmpthese -2 => {
    goatse => sub {my $count =()= make_list; $count == 1001 or die},
    count  => sub {my $count = count make_list; $count == 1001 or die},
};

给出:

         Rate goatse  count
goatse 3889/s     --   -26%
count  5276/s    36%     --

我猜测sub为什么更快是因为子程序调用被优化为传递列表而不复制它们(作为别名传递)。

答案 2 :(得分:3)

如果需要在列表上下文中运行某些内容,则必须在列表上下文中运行它。在某些情况下,就像你提出的那种情况一样,你可能可以用另一种方法解决它,但在大多数情况下你不会。

然而,在您进行基准测试之前,最重要的问题是“它是否重要?”。在您进行基准测试之前的配置文件,只有在您遇到实际问题时才会担心这些问题。 :)

如果你正在寻找最高效率,Perl的水平有点过高。 :)