почему вызовы функций в циклах Perl такие медленные? - PullRequest
15 голосов
/ 15 июля 2010

Я писал парсер файлов на Perl, поэтому пришлось перебирать файл. Файл состоит из записей фиксированной длины, и я хотел создать отдельную функцию, которая анализирует данную запись и вызывает эту функцию в цикле. Тем не менее, с большими файлами конечный результат оказался медленным, и я предположил, что мне не следует использовать внешние функции. Поэтому я сделал несколько фиктивных тестов с и без вызова функции в цикле:

[A]

foreach (1 .. 10000000) {
$a = &get_string();
}

sub get_string {
return sprintf("%s\n", 'abc');
}

[B]

foreach (1 .. 10000000) {
$a = sprintf "%s\n", 'abc';
}

Измерения показали, что код A работает примерно в 3-4 раза медленнее, чем код B. Я заранее знал, что код A должен работать медленнее, но все же я был удивлен, что разница настолько велика. Также пытался запустить похожие тесты с Python и Java. В коде Python эквивалент был примерно на 20% медленнее, чем B, а код Java работал более или менее с той же скоростью (как и ожидалось). Изменение функции с sprintf на что-то другое не показало существенной разницы.

Есть ли способ помочь Perl быстрее запускать такие циклы? Я делаю что-то здесь неправильно или это особенность Perl в том, что вызовы функций такие накладные?

Ответы [ 4 ]

26 голосов
/ 15 июля 2010

Вызовы функций Perl медленные. Это отстой, потому что именно то, что вы хотите делать, декомпозируя ваш код на поддерживаемые функции, - это то, что замедлит вашу программу. Почему они медленные? Perl делает много вещей, когда входит в подпрограмму, в результате он чрезвычайно динамичен (т. Е. Вы можете связываться со многими вещами во время выполнения). Он должен получить ссылку на код для этого имени, проверить, что это код ссылки, установить новый лексический блокнот (для хранения my переменных), новую динамическую область (для хранения local переменных), настроить @_ чтобы назвать несколько, проверьте, в каком контексте он вызывался, и передайте возвращаемое значение. Были предприняты попытки оптимизировать этот процесс, но они не окупились. См. pp_entersub в pp_hot.c для подробной информации.

Также в 5.10.0 была ошибка в функциях замедления. Если вы используете 5.10.0, обновите.

В результате избегайте многократного вызова функций в длинном цикле. Особенно если его вложенный. Можете ли вы кешировать результаты, возможно, используя Memoize ? Работу нужно выполнять внутри цикла? Это должно быть сделано внутри самого внутреннего цикла? Например:

for my $thing (@things) {
    for my $person (@persons) {
        print header($thing);
        print message_for($person);
    }
}

Вызов на header можно переместить из цикла @persons, сократив количество вызовов с @things * @persons до @things.

for my $thing (@things) {
    my $header = header($thing);

    for my $person (@persons) {
        print $header;
        print message_for($person);
    }
}
13 голосов
/ 15 июля 2010

Если ваша подпрограмма не имеет аргументов и является константой, как в вашем примере, вы можете получить значительное ускорение, используя пустой прототип "()" в подпрограмме:

sub get_string() {
    return sprintf(“%s\n”, ‘abc’);
}

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

Вы узнаете этот совет и многие другие, прочитав perlsub .

Вот тест:

use strict;
use warnings;
use Benchmark qw(cmpthese);

sub just_return { return }
sub get_string  { sprintf "%s\n", 'abc' }
sub get_string_with_proto()  { sprintf "%s\n", 'abc' }

my %methods = (
    direct      => sub { my $s = sprintf "%s\n", 'abc' },
    function    => sub { my $s = get_string()          },
    just_return => sub { my $s = just_return()         },
    function_with_proto => sub { my $s = get_string_with_proto() },
);

cmpthese(-2, \%methods);

и его результат:

                          Rate function just_return   direct function_with_proto
function             1488987/s       --        -65%     -90%                -90%
just_return          4285454/s     188%          --     -70%                -71%
direct              14210565/s     854%        232%       --                 -5%
function_with_proto 15018312/s     909%        250%       6%                  --
8 голосов
/ 15 июля 2010

Проблема, которую вы поднимаете, не имеет ничего общего с циклами. И ваши A и B примеры одинаковы в этом отношении. Скорее проблема заключается в разнице между прямым встроенным кодированием и вызовом одного и того же кода через функцию.

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

use strict;
use warnings;
use Benchmark qw(cmpthese);

sub just_return { return }
sub get_string  { my $s = sprintf "%s\n", 'abc' }

my %methods = (
    direct      => sub { my $s = sprintf "%s\n", 'abc' },
    function    => sub { my $s = get_string()          },
    just_return => sub { my $s = just_return()         },
);

cmpthese(-2, \%methods);

Вот что я получаю на Perl v5.10.0 (многопоточность MSWin32-x86). Грубо говоря, простой вызов функции, которая ничего не делает, обходится так же дорого, как прямой запуск нашего sprintf кода.

                 Rate    function just_return      direct
function    1062833/s          --        -70%        -71%
just_return 3566639/s        236%          --         -2%
direct      3629492/s        241%          2%          --

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

1 голос
/ 18 сентября 2013

Оптимизатор perl постоянно сворачивает вызовы sprintf в вашем примере кода.

Вы можете отменить его, чтобы увидеть, как это происходит:

$ perl -MO=Deparse sample.pl
foreach $_ (1 .. 10000000) {
    $a = &get_string();
}
sub get_string {
    return "abc\n";
}
foreach $_ (1 .. 10000000) {
    $a = "abc\n";
}
- syntax OK
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...