Thursday, May 08, 2014

When a Tradeoff is not a Tradeoff


Years ago, I wrote a post on clever code and advised against it, saying that instead we should strive to make our code maintainable. Since then, I gave a talk at DCBPW called Writing Maintainable Perl. During the talk, I broke a single line of code into multiple lines of code. A member of the audience pointed out that this could have performance ramifications because every time there is a semicolon, Perl does some cleanup and other maintenance tasks. I don’t know much about Perl internals. I have since looked around for more info on this, but Google was not my friend in this case, and perlguts didn’t seem to have what I was looking for in this department either.

In the real world this type of optimization is unlikely to make much of a difference. If you have code that’s dealing with I/O from users, databases, filesystems, etc, an internal language mechanism is just not going to be a factor.

Anyway… thinking back to Damian Conway’s book Perl Best Practices, I remembered a suggestion: “Don’t optimize; benchmark.” whose accompanying text proposed that instead of looking through the code for things you might improve, you should instead run Benchmark tests against your code and see how it performs.

I decided to put both versions of the code to the test and see how they perform. Here are the results:

10:54 dbradford@dbradford-PC presentation ] > ./both_listifys.pl
Benchmark: timing 5000000 iterations of good_listify, orig_listify...
good_listify: 20 wallclock secs (19.75 usr +  0.19 sys = 19.94 CPU) @
250789.99/s (n=5000000)
orig_listify: 23 wallclock secs (22.01 usr +  0.62 sys = 22.64 CPU) @
220887.08/s (n=5000000)

“good_listify” is the cleaned up version of the routine and “orig_listify” is the “bad” version. As you can see, the difference was negligible - when I ran the test five million times there was only a difference of three seconds. However, it was a surprise to me to see that the more maintainable version was faster.

Sometimes it seems like we might trade away speed to get maintainability, but if you go back and check, you might find it doesn’t have to be a tradeoff at all. Taken to a wider view: don’t theorize; demonstrate!

Here is the code that produced the output above:

#!/usr/bin/perl

use strict;
use warnings;

use Benchmark qw(:all) ;
use Data::Dumper;

sub orig_listify {
   my ($aref,$cc) = @_;
   if( ref $aref eq 'ARRAY' && $cc > 0 ) {
       my $j;
       for(my $i=0; $i<=$#$aref; $i+=$cc) {
           push @$j, [@$aref[$i..$i+$cc-1]];
       }

       # BAD!
       $#{$j->[$#{$j}]}=$#$aref%$cc;

       @$aref = @$j;
       return 1;
   }
   return;
}

sub good_listify {
   my ( $in_aref, $elements_per_array ) = @_;
   return if (
       ref $in_aref ne 'ARRAY' or
       $elements_per_array <= 0
   );
   my @result_array;
   for( my $i = 0; $i <= $#$in_aref; $i += $elements_per_array ) {
       push @result_array, [
           @$in_aref[ $i..$i + $elements_per_array - 1 ]
       ];
   }
   my $final_aref        = $result_array[ -1 ];
   my $elements_in_final = $#$in_aref % $elements_per_array;
   # Truncate final array
   $#$final_aref = $elements_in_final;
   @$in_aref = @result_array;
}

my @my_array = ( 'one', 'two', 'three', 'four', 'five', 'six', 'seven',
'eight', 'nine', 'ten' );

my $count=5_000_000;

timethese($count, {
    'orig_listify' => sub { orig_listify(\@my_array, 4) },
    'good_listify' => sub { good_listify(\@my_array, 4) },
});