in reply to Re^4: Refactoring a large script
in thread Refactoring a large script
Here's my attempt at refactoring your WEED() sub. As I don't have data, this has been done blind. Ie. Without the tests you would normally use to ensure that it continues to function correctly and that changes aimed at improving performance actually achieve that goal. So, I'll step through the changes I've made, so that you can apply and test them as you go. It should also give you some ideas for refactoring the other subs that need it.
The first thing I did was to do some trivial reformatting of the code to make the indentation consistent, add some horizontal whitespace and reduce the vertical whitespace. These changes allow me to get a clearer overview of the structure of the sub and (I find) make it easier to read. Along the way, I've also remove the comments as I find them a distraction. Obviously, you don't need to make these changes,
One anomaly that jumped out at me as I manually reformatted the code I got from your scratchpad was this line:
WEEDLOW: for my $low ( @{ $matchesfasta_id{ $site } } ) {
As there is no other reference to a variable named $matchesfasta_id in the file, and there is a line a couple above this line:
last unless @{$matches{$fasta_id}{$site}};
I've assumed that this is a typo/transcription error and should read:
WEEDLOW: for my $low ( @{ $matches{ $fasta_id }{ $site } +}) {
I've posted my starting point after the reformatting below:
The first things I did were simple clean ups of various things in the code. Starting from the top:
The first and last of these declared variables are used as loop iterators, so it is much better to declare them in-line with the loops.
The middle one is not used and can be done away with.
The first three of these are used only within the loops and are never referred to outside of the scope at which they first appear, so they again can be declared inline with their first usage.
The last three are not used anywhere in the sub and can be dropped.
Now I started looking at the function of the code and the first thing I noticed is that you sort the keys of %matches in the for loop at A below, and again each time around the nested inner loop at B.
for my $site ( sort { $a <=> $b } keys %{ $matches{ $fasta_id } } ) { + ## A last unless @{ $matches{ $fasta_id }{ $site } }; WEEDLOW: for my $low ( @{ $matches{ $fasta_id }{ $site } } ) { my $lowerlimit = $low + 0; my $upperlimit = $span + $lowerlimit; for my $sitekey ( sort { $a <=> $b } keys %{ $matches{ $fasta_ +id } } ) { ## B
Since you are not modifying %matches between these two points (or at all), then it means that you are sorting and re-sorting these keys many times. It's not easy to tell in the absence of test data how big this hash is, or how many time these loops iterate, but given the sizes of the data files mentioned in the OP, and the timing information you've supplied above, it appears that they may be substantial. I could well see this needless re-sorting being the source of a substantial amount of the runtime of the program.
Eliminating this duplication is easy and should produce a substantial performance benefit. Replacing the above lines with these should do the trick:
my @sortedKeys = sort { $a <=> $b } keys %{ $matches{ $fasta_i +d } }; for my $site ( @sortedKeys ) { last unless @{ $matches{ $fasta_id }{ $site } }; WEEDLOW: for my $low ( @{ $matchesfasta_id{ $site } } ) { my $lowerlimit = $low + 0; my $upperlimit = $span + $lowerlimit; for my $sitekey ( @sortedKeys ) {
The next thing I looked at was the section of code where you 'band pass' filter the matches into a temporary array. You then conditionally copy that temporary array into your results array, or discard the entire super set if nothing made it through the filter:
for my $hit ( @{ $matches{ $fasta_id }{ $sitekey } } ) { next unless $hit >= $lowerlimit; last unless $hit <= $upperlimit; push @arrayA, $hit + 0; } if( @arrayA ) { @{ $sets{ $fasta_id }[ $setscounter ]{ $sitekey } } = @arrayA; } else { %{ $sets{ $fasta_id }[ $setscounter ] } = (); }
Whilst there is nothing inherently wrong with this, Perl has a general purpose array filtering mechanism, namely grep specifically for this purpose and it is usually considerably quicker than an explicit loop:
@{ $sets{ $fasta_id }[ $setscounter ]{ $sitekey } } = grep { $_ >= $lowerlimit and $_ <= $upperlimit } @{ $matches{ $fasta_id }{ $sitekey } } unless( @{ $sets{ $fasta_id }[ $setscounter ]{ $sitekey } } ) { undef $sets{ $fasta_id }[ $setscounter ] }; next WEEDLOW; }
There is a possible caveat with this change! Your explicit loop does allow you to short circuit the loop at the high pass limit which isn't possible with grep. However, as shown above, using grep does allow you to avoid the creation of and later copying of the temporary array. This combined with greps inherent better performance may outweigh the benefit of that short circuiting. Or it may not.
The only way to tell will be to make the change and benchmark. If the size of the array being filtered is sufficiently large, and the pass band sufficiently narrow and low down in the range that the short circuit is beneficial, then it would be better to use a binary search algorithm to discover the low and high range limits and then copy the values across to the results set using an array slice.
I haven't coded that as I have no way to make the test.
Finally (for now), you use this construct:
%{ $sets{ $fasta_id }[ $setscounter ] } = ();
To empty arrays in several places. Whilst there is nothing wrong with this, I think it is clearer, and may be slightly quicker to use
undef $sets{ $fasta_id }[ $setscounter ] };
and I've made those changes also.
The final code is the second of the two code blocks below.
There are various other things that look suspect.
# @fastarray = (); @fastarray = @newfast;
BTW: There is no point in emptying the array before copying over it. The first line above is redundant.
push @newfast, $h;
if( scalar keys %{ $sets{ $fasta_id }[ $setscounter ] } < $num ) {
This should be passed in as a parameter, and should certainly have a better name.
This is probably a holdover from the pervasive use of globals in its previous incarnation and avoiding these globals make good sense. However, passing entire hashes is expensive if they are large (as these appear to be).
It would be more efficient to pass hash references in and out, but there is a downside to that in the additional complexity it creates in the referencing of these complex structures. There is also a small performance hit as a result.
There is a solution to this which involves using a localised glob to alias the hash references and so gain the benefits of avoiding copying, whilst retaining the simpler referencing to the structures. However, for reasons that completely bewilder me, this use of globs is generally seen as 'bad thing', so I won't recommend it here.
There are also various cpan modules that can provide a similar aliasing facility. If you can get one of those to build and install on your system, something which I have failed to achieve on mine, then you could investigate using one of them.
for my $checkhash ( keys %{ $sets{ $fasta_id }[ $setscounter ] + } ) { unless( defined $sets{ $fasta_id }[ $setscounter ]{ $check +hash }[ 0 ] ) { undef $sets{ $fasta_id }[ $setscounter ] }; last; } } if( scalar keys %{ $sets{ $fasta_id }[ $setscounter ] } < $num + ) { undef $sets{ $fasta_id }[ $setscounter ]; } $setscounter++ if scalar %{ $sets{ $fasta_id }[ $setscounter ] + }; } } if( @{ $sets{ $fasta_id } } ) { pop @{ $sets{ $fasta_id } } unless scalar %{ $sets{ $fasta_id }[ $ +#{ $sets{ $fasta_id } } ] }; }
If the changes I've made so far haven't broken anything and achieve the greater than 50% speedup of this sub that I estimate, then I would look carefully at these lines to see if what they are doing can be improved.
HTH
My starting point after the trivial reformating I mentioned at the top:
sub WEED { my( $span, %matches) = @_; my( $site, $fasta, $sitekey) = ''; my( $setscounter, $lowerlimit, $upperlimit, $i, $set, $yes ) = 0; my %sets = (); my @newfast; for my $fasta_id ( @fastheaders ) { $setscounter = 0; next unless defined %{ $matches{ $fasta_id } }; print "Currently analysing results from sequence:\n $fasta +_id\n"; for $site ( sort { $a <=> $b } keys %{ $matches{ $fasta_id } } + ) { last unless @{ $matches{ $fasta_id }{ $site } }; $i = 0; WEEDLOW: for my $low ( @{ $matches{ $fasta_id }{ $site } } + ) { $lowerlimit = $low + 0; $upperlimit = $span + $lowerlimit; for $sitekey ( sort { $a <=> $b } keys %{ $matches{ $f +asta_id } } ) { next unless defined @{ $matches{ $fasta_id }{ $sit +ekey } }; my @arrayA = (); for my $hit ( @{ $matches{ $fasta_id }{ $sitekey } + } ) { next unless $hit >= $lowerlimit; last unless $hit <= $upperlimit; my $ggg = $hit + 0; push( @arrayA, $ggg); $ggg = 0; next; } if( @arrayA ) { @{ $sets{ $fasta_id }[ $setscounter ]{ $siteke +y } } = @arrayA; @arrayA = (); } else { %{ $sets{ $fasta_id }[ $setscounter ] } = (); next WEEDLOW; } } for my $checkhash ( keys %{ $sets{ $fasta_id }[ $setsc +ounter ] } ) { unless( defined $sets{ $fasta_id }[ $setscounter ] +{ $checkhash }[ 0 ] ) { %{ $sets{ $fasta_id }[ $setscounter ] } = (); last; } } if( scalar keys %{ $sets{ $fasta_id }[ $setscounter ] +} < $num ) { %{ $sets{ $fasta_id }[ $setscounter ] } = (); } $setscounter++ if scalar %{ $sets{ $fasta_id }[ $setsc +ounter ] }; } } if( @{ $sets{ $fasta_id } } ) { pop @{ $sets{ $fasta_id } } unless scalar %{ $sets{ $fasta +_id }[ $#{ $sets{ $fasta_id } } ] }; } if( @{ $sets{ $fasta_id } } ) { push @newfast, $h; } else { delete $sets{ $fasta_id }; } } @fastarray = (); @fastarray = @newfast; return %sets; }
And the refactored code as far as I've taken it:
sub WEED { my( $span, %matches) = @_; my %sets; my @newfast; for my $fasta_id ( @fastheaders ) { my $setscounter = 0; next unless defined %{ $matches{ $fasta_id } }; print "Currently analysing results from sequence:\n $fasta +_id\n"; my @sortedKeys = sort { $a <=> $b } keys %{ $matches{ $fasta_i +d } }; for my $site ( @sortedKeys ) { last unless @{ $matches{ $fasta_id }{ $site } }; WEEDLOW: for my $low ( @{ $matches{ $fasta_id }{ $site } } + ) { my $lowerlimit = $low + 0; my $upperlimit = $span + $lowerlimit; for my $sitekey ( @sortedKeys ) { next unless defined @{ $matches{ $fasta_id }{ $sit +ekey } }; @{ $sets{ $fasta_id }[ $setscounter ]{ $sitekey } +} = grep { $_ >= $lowerlimit and $_ <= $upperlimit } @{ $matches{ $fasta_id }{ $sitekey } } unless( @{ $sets{ $fasta_id }[ $setscounter ]{ $si +tekey } } ) { undef $sets{ $fasta_id }[ $setscounter ] }; next WEEDLOW; } } for my $checkhash ( keys %{ $sets{ $fasta_id }[ $setsc +ounter ] } ) { unless( defined $sets{ $fasta_id }[ $setscounter ] +{ $checkhash }[ 0 ] ) { undef $sets{ $fasta_id }[ $setscounter ] }; last; } } if( scalar keys %{ $sets{ $fasta_id }[ $setscounter ] +} < $num ) { undef $sets{ $fasta_id }[ $setscounter ]; } $setscounter++ if scalar %{ $sets{ $fasta_id }[ $setsc +ounter ] }; } } if( @{ $sets{ $fasta_id } } ) { pop @{ $sets{ $fasta_id } } unless scalar %{ $sets{ $fasta +_id }[ $#{ $sets{ $fasta_id } } ] }; } if( @{ $sets{ $fasta_id } } ) { push @newfast, $h; } else { delete $sets{ $fasta_id }; } } @fastarray = @newfast; return %sets; }
|
|---|
| Replies are listed 'Best First'. | |
|---|---|
|
Re^6: Refactoring a large script
by mdunnbass (Monk) on Jan 21, 2007 at 04:12 UTC | |
|
Re^6: Refactoring a large script
by mdunnbass (Monk) on Jan 23, 2007 at 15:50 UTC | |
by BrowserUk (Patriarch) on Jan 23, 2007 at 15:57 UTC | |
by mdunnbass (Monk) on Jan 23, 2007 at 16:04 UTC |