Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

why are function calls in Perl loops so slow?

I was writing a file parser in Perl, so had to loop through file. File consists of fixed length records and I wanted to make a separate function that parses given record and call that function in a loop. However, final result turned to be slow with big files and my guess was that I shouldn't use external function. So I made some dummy tests with and without function call in a loop:

[A]

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

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

[B]

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

Measuring showed that A code runs about 3-4 times slower than code B. I knew beforehand that code A was supposed to run slower but still I was surprised that difference is that big. Also tried to run similar tests with Python and Java. In Python code A equivalent was about 20% slower than B and Java code was runing more or less at the same speed (as expected). Changing function from sprintf to something else didn't show any significant difference.

Is there any way to help Perl run such loops faster? Am I doing something totaly wrong here or is it Perl's feature that function calls are such overhead?

like image 737
MariusM Avatar asked Jul 15 '10 12:07

MariusM


4 Answers

Perl function calls are slow. It sucks because the very thing you want to be doing, decomposing your code into maintainable functions, is the very thing that will slow your program down. Why are they slow? Perl does a lot of things when it enters a subroutine, a result of it being extremely dynamic (ie. you can mess with a lot of things at run time). It has to get the code reference for that name, check that it is a code ref, set up a new lexical scratchpad (to store my variables), a new dynamic scope (to store local variables), set up @_ to name a few, check what context it was called in and pass along the return value. Attempts have been made to optimize this process, but they haven't paid out. See pp_entersub in pp_hot.c for the gory details.

Also there was a bug in 5.10.0 slowing down functions. If you're using 5.10.0, upgrade.

As a result, avoid calling functions over and over again in a long loop. Especially if its nested. Can you cache the results, perhaps using Memoize? Does the work have to be done inside the loop? Does it have to be done inside the inner-most loop? For example:

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

The call to header could be moved out of the @persons loop reducing the number of calls from @things * @persons to just @things.

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

    for my $person (@persons) {
        print $header;
        print message_for($person);
    }
}
like image 163
Schwern Avatar answered Nov 02 '22 23:11

Schwern


If your sub has no arguments and is a constant as in your example, you can get a major speed-up by using an empty prototype "()" in the sub declaration:

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

However this is probably a special case for your example that do not match your real case. This is just to show you the dangers of benchmarks.

You'll learn this tip and many others by reading perlsub.

Here is a benchmark:

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

and its result:

                          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%                  --
like image 27
dolmen Avatar answered Nov 02 '22 23:11

dolmen


The issue you are raising does not have anything to do with loops. Both your A and B examples are the same in that regard. Rather, the issue is the difference between direct, in-line coding vs. calling the same code via a function.

Function calls do involve an unavoidable overhead. I can't speak to the issue of whether and why this overhead is costlier in Perl relative to other languages, but I can provide an illustration of a better way to measure this sort of thing:

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

Here's what I get on Perl v5.10.0 (MSWin32-x86-multi-thread). Very roughly, simply calling a function that does nothing is about as costly as directly running our sprintf code.

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

In general, if you need to optimize some Perl code for speed and you're trying to squeeze out every last drop of efficiency, direct coding is the way to go -- but that often comes with a price of less maintainability and readability. Before you get into the business of such micro-optimizing, however, you want to make sure that your underlying algorithm is solid and that you have a firm grasp on where the slow parts of your code actually reside. It's easy to waste a lot of effort working on the wrong thing.

like image 23
FMc Avatar answered Nov 03 '22 01:11

FMc


The perl optimizer is constant-folding the sprintf calls in your sample code.

You can deparse it to see it happening:

$ 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
like image 40
salva Avatar answered Nov 02 '22 23:11

salva