Beefy Boxes and Bandwidth Generously Provided by pair Networks
Syntactic Confectionery Delight
 
PerlMonks  

When every microsecond counts: Parsing subroutine parameters

by snowhare (Friar)
on May 17, 2008 at 18:43 UTC ( [id://687119]=perlmeditation: print w/replies, xml ) Need Help??

Here is something for people who are trying to optimize their code for performance without necessarily going all the way to inlining all your performance critical sections.

Parameter hashes are really expensive.

If you wander around the CPAN, you will notice that almost everyone loves parameter hashes. There are good reasons for that. It is definitely easier to read, maintain and extend something like

Search::InvertedIndex::Update->new({ -group => $group, -index => $index, -data => $index_data, -keys => { $key0 => 10, $key1 => 20, $key2 => 15, }, });
Than say
Search::InvertedIndex::Update->new($group, $index, $index_data, { $key0 => 10, $key1 => 20, $key2 => 15, } );

Especially if you have optional parameters or many parameters. But what are you trading for that clarity and those flexible interfaces?

Runtime speed. And more than you might think.

While putting the final touches on a parameter hash parsing module (Acme::Sub::Parms) I just released after two years of considering whether or not I should release at all (and finally put in Acme because of its use of source filtering), I had reason to benchmark the different ways of parsing parameter hashes. And the performance losses can be stunning.

Consider a simple function taking two parameters that are assigned to two lexical variables without validation. Here are some of the ways that it could be implemented:

You could use Params::Validate (with validation turned off to speed it up)

use Params::Validate qw (validate); $Params::Validate::NO_VALIDATION = 1; sub params_validate { my ($handle, $thing) = @{(validate(@_, { handle => 1, thing => 1 } +))}{'handle','thing'}; }
or you could use Class::ParmList
use Class::ParmList qw (simple_parms); sub simple_parms_args { my ($handle, $thing) = simple_parms(['handle','thing'], @_); }
or my new Acme ('Acme, when you probably shouldn't have, but couldn't help yourself') module, Acme::Sub::Parms (again with validation turned off)
use Acme::Sub::Parms qw(:no_validation); sub sub_parms_bindparms { BindParms : ( my $handle : handle; my $thing : thing; ) }
Or you could use hand rolled code:
sub one_step_args { my ($handle, $thing) = @{{@_}}{'handle','thing'}; }
A little less obscurely you could use:
sub std_args { my %args = @_; my ($handle, $thing) = @args{'handle','thing'}; }
If you wanted to be fancy, the hand rolled code could even be case-insensitive:
sub caseflat_std_args { my %args; { my %raw_args = @_; %args = map { lc($_) => $raw_args{$_} } keys %raw_args; } my ($handle, $thing) = @args{'handle','thing'}; }
Finally, you could abandon the parameter hash for simple positional parameters:
sub positional_args { my ($handle, $thing) = @_; }
Rolling these up into a Benchmark script you get
#!/usr/bin/perl use strict; use warnings; use Acme::Sub::Parms qw(:no_validation); use Class::ParmList qw (simple_parms); use Params::Validate qw (validate); use Benchmark qw(cmpthese); $Params::Validate::NO_VALIDATION = 1; cmpthese(1000000, { 'bindparms' => sub { sub_parms_bindparms( handle => 'Test', 'th +ing' => 'something')}, 'std_args' => sub { std_args( handle => 'Test', 'thing' => 'so +mething')}, 'caseflat' => sub { caseflat_std_args( handle => 'Test', 'thin +g' => 'something')}, 'one_step' => sub { one_step_args( handle => 'Test', 'thing' = +> 'something')}, 'postnl_args' => sub { positional_args( 'Test', 'something')}, 'simple_parms' => sub { simple_parms_args( handle => 'Test', 'thin +g' => 'something')}, 'validate' => sub { params_validate( handle => 'Test', 'thing' + => 'something')}, } ); exit; ###################################################################### +###### sub params_validate { my ($handle, $thing) = @{(validate(@_, { handle => 1, thing => 1 } +))}{'handle','thing'}; } sub sub_parms_bindparms { BindParms : ( my $handle : handle; my $thing : thing; ) } sub simple_parms_args { my ($handle, $thing) = simple_parms(['handle','thing'], @_); } sub positional_args { my ($handle, $thing) = @_; } sub one_step_args { my ($handle, $thing) = @{{@_}}{'handle','thing'}; } sub caseflat_std_args { my %args; { my %raw_args = @_; %args = map { lc($_) => $raw_args{$_} } keys %raw_args; } my ($handle, $thing) = @args{'handle','thing'}; } sub std_args { my %args = @_; my ($handle, $thing) = @args{'handle','thing'}; }
And the resulting numbers?
Rate validate simple_parms caseflat bindparms one_ste +p std_args postnl_args validate 24851/s -- -40% -74% -90% -90 +% -92% -97% simple_parms 41203/s 66% -- -57% -83% -84 +% -86% -96% caseflat 95969/s 286% 133% -- -62% -62 +% -68% -90% bindparms 249377/s 903% 505% 160% -- -1 +% -16% -73% one_step 251889/s 914% 511% 162% 1% - +- -15% -73% std_args 296736/s 1094% 620% 209% 19% 18 +% -- -68% postnl_args 925926/s 3626% 2147% 865% 271% 268 +% 212% --

Ouch. 'validate' is probably the most popular parameter parser. And it is molasses in winter slow. 'simple_parms' is faster, but only in relative terms. 'BindParms' is sort of ok, as are the hand coded parsers for parameter hashes (with the exception of the case flattening one), but the winner by several horse lengths is the positional parameters. It is 36 times faster than 'validate and 3.5 times faster than the fastest of the hand code parameter hash parsers.

Lesson: When performance is on the line for a subroutine, use positional parameters NOT parameters hashes.

I had this lesson driven home while writing Search::InvertedIndex when I ran the code through DProf. Changing just two performance critical subroutines from parameter hashes parsed using 'simple_parms' to positional parameters roughly tripled the performance of the entire module while indexing.

And that is the other lesson: Use code profilers to identify your performance bottlenecks. You are likely to be surprised by where you are losing most of your cycles.

Update: Since the question of the cost of the sub call itself was raised, I'm appending a benchmark for testing the sub calls impact.

#!/usr/bin/perl use strict; use warnings; my @parms = ('handle','thing'); @_ = ('handle','thing'); use Benchmark qw(cmpthese); cmpthese(2000000, { 'one_sub' => \&one_sub, 'anon_sub' => sub { my ($handle, $thing) = @_; }, 'double_sub' => sub { one_sub(); }, 'parm2_sub' => sub { double_sub('handle','thing'); }, 'std_args' => sub { std_args('handle','thing'); }, 'std_args_d' => sub { std_args('handle','thing'); }, } ); exit; ###################################################################### +###### sub one_sub { my ($handle, $thing) = @_; } sub d_sub { "1"; } sub double_sub { d_sub(@_); my ($handle, $thing) = @_; } sub std_args_d { my %args = @_; d_sub(@_); my ($handle, $thing) = @args{'handle','thing'}; } sub std_args { my %args = @_; my ($handle, $thing) = @args{'handle','thing'}; }
And the results:
Rate std_args_d std_args parm2_sub double_sub anon_sub + one_sub std_args_d 392157/s -- -0% -34% -65% -84% + -85% std_args 393701/s 0% -- -33% -64% -84% + -85% parm2_sub 589971/s 50% 50% -- -47% -76% + -78% double_sub 1104972/s 182% 181% 87% -- -55% + -59% anon_sub 2469136/s 530% 527% 319% 123% -- + -9% one_sub 2702703/s 589% 586% 358% 145% 9% + --

As you can see, the sub call itself doesn't matter that much. The overhead of decoding parameter hashes is much larger than the overhead of the sub call. And the overhead of the sub call is only somewhat larger (perhaps 1.5 to 1.7 times) than the size of the overhead of decoding the positional parameters alone.

Update2: Put some 'readmore' sections around the benchmarking code chunks.

Replies are listed 'Best First'.
Re: When every microsecond counts: Parsing subroutine parameters
by samtregar (Abbot) on May 17, 2008 at 20:32 UTC
    Take home: every single method here is really, really fast. 24k calls per second is not slow. You'd have to make some fairly radical mistakes in your API before that would ever matter!

    If parameter parsing is really a bottleneck in your application then you're probably making too many subroutine calls! Much better than trying to tune parameter setup is to simply write methods that do work over sets of data. So instead of something like:

    foreach my $data (@rows) { $processor->do_one(data => $data); }

    Add support for:

    $processor->do_all(rows => \@rows);

    But the vast majority of applications out there are not CPU bound to begin with - they're I/O bound either talking to a network, the filesystem or a database. In any of those cases you'd be pretty unlikely to see parameter parsing show up on a profile - I use Params::Validate all the time and I've never seen it show up.

    -sam

      Well, I recall reading something about Plucene, the Perl port of Lucene, being very difficult to get performant. After optimization it was uniformly slow because of many method calls. This wasn't really a problem in Java but was a problem in Perl.

      (this is what I recall, a quick Google session didn't find me the mail or post I remember reading about this. Plucene developers would obviously know the real story here.)

      But it is interesting what we do with this issue. Named parameters is a very common idiom. It is a very good idiom, in that it leads to maintainable code.

      So, what can we do to make it perform better? Some special optimization of this case in the perl implementation? Some new syntax to support this idiom? Could it be related to the new named arguments being proposed for perl 5.12?

      /J

        I don't think any optimization would help much and thus will not be implemented. I ran a few benchmarks to see how much of the additional overhead is related to the repeated creation of the hash and thus might be removed by reusing the hash:

        use Benchmark qw(cmpthese); sub with_hash { my ($one, $two) = @{$_[0]}{'one', 'two'}; } sub wo_hash { my ($one, $two) = @{{@_}}{'one', 'two'}; } my %h = (one => undef, two => undef); cmpthese(1000000, { wo_hash => sub { wo_hash(one => 7, two => 9) }, with_hash => sub { with_hash({one => 7, two => 9}) }, with_consthash => sub { with_hash(\%h) }, with_consthash_mod => sub { @h{'one','two'} = (8,1); with_hash(\%h +) }, with_consthash_modd => sub { @h{'one','two'} = (8,1); with_hash(\% +h); @h{'one','two'}=() }, with_consthash_moddL => sub { local @h{'one','two'} = (8,1); with_ +hash(\%h);}, with_consthash_moddRA => sub { @h{'one','two'} = (8,1); my @r=with +_hash(\%h); @h{'one','two'}=(); @r }, with_consthash_moddRS => sub { @h{'one','two'} = (8,1); my $r=with +_hash(\%h); @h{'one','two'}=(); $r }, });
        As you can see I tried to pass a completely constant hash, that looked OK, much better than foo({one => 1, two => 2}) (1002004/s vs 424628/s on my computer with Perl 5.8.8), the problem is that once I modified the values in the hash before the call the gain got much smaller (634921/s vs 424628/s). And the problem was that the values were kept in the hash between invocations ... which doesn't matter for numbers, but would matter for huge strings or for references. So I had to clear the values. undef()ing the whole hash destroyed any gain whatsoever, setting the values to undef took me to just 489237/s vs 424628/s. And that was if the called subroutine did not need to return anything!

        I tried to use local() on the hash slice or assign the return value into a variable, but that just made things worse, in case the function was supposed to return a list, even worse than the normal inline hash version.

        So even if perl created a hash for the subroutine just once, kept it and just modified and removed the values for each call, the speed gain would be fairly small. For a fairly high price both in memory footprint and code complexity.

        The only thing that might really help would be to convert the named parameter calls into positional on compile time. The catch is that it would require that Perl knows, at the time it compiles the call, all named parameters the subroutine/method can take and the order in which they are expected while converted to positional. Which is completely out of question for methods.

        I'm afraid we have to live with the overhead and in the rare case it actually matters, change the subroutine/method to use positional params.

        So, what can we do to make it perform better?

        If by "it" you mean "our programs" I think the answer is simple - make fewer subroutine calls. If your program is making so many tiny do-nothing calls that parameter parsing or even just subroutine overhead is a significant factor then you're just making too many calls.

        It's a fact of life in Perl that method calls cost something (not too much, but not nothing either). That just means you need to make them count!

        -sam

        Here's a write-up of the Plucene method call issue.

        Everything in Lucene is a method, down to outstream.writeByte(). Hash-based method dispatch just isn't fast enough for a straight port.

        --
        Marvin Humphrey
        Rectangular Research ― http://www.rectangular.com
        ... because of many method calls. This wasn't really a problem in Java but was a problem in Perl.

        Java makes inline expansion optimizations (as C does with the inline keyword or in gcc somehow automatically with the -O3 flag).

Re: When every microsecond counts: Parsing subroutine parameters
by shmem (Chancellor) on May 17, 2008 at 19:02 UTC

    When every microsecond counts you factor out expensive stuff into XS code. Or don't use perl.

    That said, constructing (and tearing down) a hash (which happens if you call foo( {bar => 'quux'} )) at every subroutine invocation is expensive, but calling a sub in the first place is probably more expensive (stack frame setup / enter / leave / unwind cycle).

    --shmem

    _($_=" "x(1<<5)."?\n".q·/)Oo.  G°\        /
                                  /\_¯/(q    /
    ----------------------------  \__(m.====·.(_("always off the crowd"))."·
    ");sub _{s./.($e="'Itrs `mnsgdq Gdbj O`qkdq")=~y/"-y/#-z/;$e.e && print}
      XS has a surprising amount of overhead per call copying parameters in and out. When I was writing Lingua::Stem I was able to achieve 80% of the speed of Lingua::Stem::Snowball (which uses XS) for stemming in place and actually beat Lingua::Stem::Snowball by 30% for large straight list processing.
        All parameter passing / subroutine calls involving perl are relatively slow, and conversion to and from C types can probably make XS even slower than pure perl if used naively. The best way to use XS/C is to reduce the number of calls back and forth (IOW, move your iterations to C code, don't loop over XS calls where speed really matters). C compilers are very good at inlining and otherwise optimizing subroutine calls. Perl isn't, really - even though it's overall pretty damn fast compared to some other "scripting" languages I could mention.

Re: When every microsecond counts: Parsing subroutine parameters
by adamk (Chaplain) on May 19, 2008 at 04:42 UTC
    I think there might be a small mistake in one of your examples.

    The entire point of doing "my %args = @_" is that you then don't HAVE to extract them to their own variables. Granted, you pay incrementally per invocation of $args{...} but you don't pay for the ones you don't use. And if you only use a param once, it's cheaper than allocating it to a variable.

    So the slice and allocation you have there is probably superfluous

    Named params is also a good way to support subclassing, because you don't have to co-ordinate with child classes.

    You just $self->SUPER::method(@_) or $self->SUPER::method(%args);

    I'll also note that, for a number of position params two or smaller, it's faster to have two separate "my $var = shift;" calls than one "my ($var, $var) = @_;" call.
Re: When every microsecond counts: Parsing subroutine parameters
by Anonymous Monk on May 17, 2008 at 20:02 UTC

      They do not matter unless they do.

      One of the tasks of a developer is to know when do they mater, the task of profiling tools is to tell him where and the task of benchmarks is to tell him how.

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perlmeditation [id://687119]
Approved by Corion
Front-paged by holli
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others having an uproarious good time at the Monastery: (4)
As of 2024-04-19 21:21 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found