Подтвердить что ты не робот

Эффективен ли секретный оператор Perl Goatse?

"Оператор goatse" или идиома =()= в Perl заставляет выражение оцениваться в контексте списка.

Пример:

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";

Когда я интерпретирую использование, это происходит:

  • $str =~ /\d/g соответствует всем цифрам. Контекст g и контекст списка выдает список этих совпадений. Пусть это будет пример "List Producer", а в Perl это может быть много.
  • =()= вызывает присвоение пустого списка, поэтому все фактические совпадения копируются в пустой список.
  • Назначение в скалярном контексте к счету $списка, созданного в 2., дает счетчик списка или результат 5.
  • Счетчик ссылок пустого списка =()= переходит в нуль после скалярного присваивания. Затем копия элементов списка удаляется Perl.

Вопросы эффективности:

  • Неужели я ошибаюсь в том, как я разбираю это?
  • Если у вас есть List Producer, и все, что вас интересует, это счет, есть ли более эффективный способ сделать это?

Он отлично работает с этим тривиальным списком, но что, если в списке было сотня тысяч матчей? С помощью этого метода вы создаете полную копию каждого совпадения, а затем удаляете его только для подсчета.

4b9b3361

Ответ 1

Perl 5 умеет копировать списки. Он копирует только столько элементов, сколько находится на левой стороне. Он работает, потому что назначение списка в скалярном контексте дает количество элементов в правой части. Таким образом, элементы n будут создаваться регулярным выражением, но они не будут скопированы и отброшены, просто отброшены. Вы можете увидеть разницу, которую добавляет дополнительная копия в наивном случае в сравнительном диалоговом окне ниже.

Что касается эффективности, то итеративное решение часто бывает проще в использовании памяти и процессора, но это должно быть сопоставимо с сжатием секретного оператора goatse. Ниже приведены результаты сравнительного анализа различных решений:

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;
}

Ответ 2

В вашем конкретном примере полезен тест:

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 имеет регулярное выражение в скалярном (булевом) контексте, поэтому механизм regex просто должен возвращать 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 быстрее, потому что вызовы подпрограмм оптимизированы для передачи списков без их копирования (передаются как псевдонимы).

Ответ 3

Если вам нужно запустить что-то в контексте списка, вы должны запустить его в контексте списка. В некоторых случаях, как и тот, который вы представляете, вы можете работать с ним другой техникой, но в большинстве случаев вы этого не сделаете.

Прежде чем вы начнете оценивать, самый важный вопрос: "Это даже имеет значение?". Профиль перед тем, как вы оцениваете, и только беспокоитесь об этих вещах, когда у вас заканчиваются реальные проблемы.:)

Если вы ищете максимальную эффективность, Perl немного слишком высокий.:)