Почему вызовы функций в циклах Perl настолько медленны?

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

[А]

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

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

[В]

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

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

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

Ответы

Ответ 1

Вызов функций Perl выполняется медленно. Это отстой, потому что то, что вы хотите делать, разлагая свой код на поддерживаемые функции, - это то, что замедлит вашу программу. Почему они медленны? Perl делает много вещей, когда он входит в подпрограмму, что является чрезвычайно динамичным (т.е. Во время выполнения вы можете возиться с множеством вещей). Он должен получить ссылку на код для этого имени, проверить, что это код ref, настроить новый лексический блокнот (для хранения переменных 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);
    }
}

Ответ 2

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

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%                  --

Ответ 3

Проблема, которую вы поднимаете, не имеет никакого отношения к циклам. Оба примера 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-multi-thread). Очень грубо говоря, просто вызов функции, которая ничего не делает, будет столь же дорогостоящей, как прямое выполнение нашего кода sprintf.

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

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

Ответ 4

Оптимизатор 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