Perl 6 small stuff #9: Vantage points and the perception of speed

This is not a post complaining about speed. It’s a post about how expectations influence our perception of speed.


Speed isn’t everything. Sometimes clarity and elegance get precedence. I think Perl 6 enables elegance, so much so that I don’t mind that it’s not a speed daemon [1]. Perl 5 on the other hand is. And we shouldn’t expect any less of it, since 5 has been trimmed and tinkered with for a quarter of a century now.

Perl 6 is the newcomer, so it’d be unreasonable to expect the same optimisation. But the thing is that guys like me, i.e. people with some Perl 5 experience, are carriers of perl5-isms. Some of those -isms spill over when we write Perl 6 code [2]. Not only do I expect the result to be the same, but if I’m honest also that it’d match some of 5’s speed.

Consider the code below.

# Perl 5
$ time perl -E 'my @a = "a".."z"; my @e = map join("", map $a[rand @a], 1..8), 1..1_000_000; say "P5: $e[0]";'
P5: glgvlxzm
real 0m3.563s
user 0m3.478s
sys 0m0.069s

This snippet generates an array of 1,000,000 eight-character strings (in itself not very interesting, but I’ll use the array later on for something else). My old-ish MacBook Pro Perl 5 uses around 3,5 seconds to generate one million strings and populate an array with them.

If I — out of habit — program the same thing in Perl 6, using my perl 5 thinking, I end up with almost similar code.

$ time perl6 -e 'my @a = "a".."z"; my @e = map { map( { @a.pick }, 1..8).join }, 1..1_000_000; say "P6: @e[0]";'
P6: uywokcsh
real 0m49.320s
user 0m49.029s
sys 0m0.296s

The result’s exactly what I expected, what’s unexpected is the time the program takes to complete [3]. Had Perl 6 been called, say, Camelia or Century or whatever else, I think I wouldn’t have thought about speed in these terms. Sure, I’d notice that it was a little sluggish. I’d maybe compare the results to Python or Julia or Perl 5 and been a little surprised, but I wouldn’t have expected 1:1 similarity speed wise.

Perhaps I’d be more occupied with the ways Perl 6 enables beautiful, readable and concise code. Because those are, to me, Perl 6’s main selling points at the moment. It’s just that as it is now, in the shadow of Perl 5, they’re a little easy to forget.

If you’re interested you can now go on to read the second part: Perl small stuff #9½: Perception of speed — benchmarking grep. In that one I discover that it’s really the small stuff that makes a big difference

One day later: Read Simon Proctor’s insightful comment below. He pointed me to the Perl 6 solution that is the fastest [5].


Notes

[1] Here’s Zoffix Znet’s excellent presentation on how to speed up Perl 6 programs. Watch it. It’s worth spending 27 1/2 minutes on.

[2] Perl 5 has become so fast over time that I get away with lots of sloppy code, something that Perl 6 won’t let me — yet. So I could flip this on its head and say that Perl 6 teaches me to write better code. The value of that can’t be underestimated.

[3] The code above can become almost 2,5x faster just by un-perl5-ifying it. Replace the my @e part of the code with this:

my @e = ((@a.pick for ^8).join("") for ^1_000_000);

This little change makes the code execute in around 20 seconds. We’re closing in on acceptable territory here, and I’m sure the excellent work done by all the volunteers making Perl 6 will just make it even better over time.

[4] The most elegant way to generate a million element array with arbitrary strings is, in my eyes, the one below. It’s the way I’d do it if I wanted to show off a little, and maybe even if I prefered the most readable code:

my @e = (pick(8, "a".."z").join for ^1_000_000);

But showing off comes with a performance penalty — obviously because “a”..”z” is generated a million times here. The execution time (52 seconds) is even slower than the Perl 5-ish Perl 6 code we started with.

The middle ground seems, as always, to be the best.

[5] Simon Proctor made me aware of two things, one I didn’t know and one I had forgot. The first is that there is a certain speed difference between pick and roll. Since it’s not mandatory for me to have eight distinct letters — repeats are acceptable — I can switch from pick to roll. The one I forgot is that the routines roll and pick can take a parameter, i.e. how many to pick or roll. That makes my for ^8 loop redundant. The pick/roll method loops faster. So…

File: optimized-gather.p6
my @alphabet = "a".."z";
my @array = (@alphabet.roll(8).join for ^1_000_000);
say "^yl: " ~ @array.grep(*.starts-with("yl")).elems;
Run: 
$ time perl6 optimized-gather.p6
Output:
^yl: 1452
real 0m8.536s
user 0m8.438s
sys 0m0.254s

This version is 5.8x faster than the one we started with, and just 2.4x times slower than the original Perl 5 version. But please note how not like Perl 5 the end result has become. This goes to show that style and other choices influence Perl 6's speed immensely. See my follow-up article for more on that.

Like what you read? Give Jo Christian Oterhals a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.