I'm on a new team at the BBC. On the previous team, PIPs, we gathered BBC programme data for television and radio. The rest of the BBC could use PIPs to pull schedules, get information about Doctor Who (note, that's "Doctor", not "Dr."!) or understand how a radio programme is broken down into segments which might be rebroadcast on a different programme. The work was complex, but fun. If our system went down, large parts of the BBC wouldn't be able to update their programme data.
On the new team, Dynamite, it's a different story. If we go down, large parts of the BBC's online presence go down. Ever visit www.bbc.co.uk/iplayer/? That's ours. Given that the BBC is one of the most heavily trafficked web sites in the world, we have to worry about performance. We count milliseconds. As a result, the team I'm on now doesn't use Moose. Ah, but you tell me Moose is fast now! Yes, Moose is fast and most of its performance issues are in the startup, not in the runtime. I'll agree with you on this, but look at this benchmark:
#!/usr/bin/env perl
{
package Foo::Moose;
use Moose;
has bar => (is => 'rw');
__PACKAGE__->meta->make_immutable;
}
{
package Foo::Manual;
sub new { bless {} => shift }
sub bar {
my $self = shift;
return $self->{bar} unless @_;
$self->{bar} = shift;
}
}
my $foo1 = Foo::Moose->new;
sub moose {
$foo1->bar(32);
my $x = $foo1->bar;
}
my $foo = Foo::Manual->new;
sub manual {
$foo->bar(32);
my $x = $foo->bar;
}
use Benchmark 'timethese';
print "Testing Perl $]\n";
timethese(
1_500_000,
{
moose => \&moose,
manual => \&manual,
}
);
Sample output:
Testing Perl 5.010001
Benchmark: timing 1500000 iterations of manual, moose...
manual: 2 wallclock secs ( 1.86 usr + 0.00 sys = 1.86 CPU) @ 806451.61/s (n=1500000)
moose: 1 wallclock secs ( 1.93 usr + 0.00 sys = 1.93 CPU) @ 777202.07/s (n=1500000)
No matter how many times I run this, we see the manual output only a hair faster than Moose. Of course, we had to avoid constructing the object in this benchmark. Otherwise, we see that object construction in Moose is slow:
Benchmark: timing 1500000 iterations of manual, moose...
manual: 5 wallclock secs ( 4.43 usr + 0.01 sys = 4.44 CPU) @ 337837.84/s (n=1500000)
moose: 6 wallclock secs ( 7.40 usr + 0.00 sys = 7.40 CPU) @ 202702.70/s (n=1500000)
(Look at the @$num/s figures).
That's not fair, though, because you construct an object once and then do lots of things with it. That being said, Moose offers so many benefits that our tiny, tiny performance hit is worth it, isn't it? Look at the original code and you'll see that we're not really taking advantage of Moose, so let's add a type check.
{
package Foo::Moose;
use Moose;
has bar => (is => 'rw', isa => 'Int');
__PACKAGE__->meta->make_immutable;
}
And the benchmark:
Benchmark: timing 1500000 iterations of manual, moose...
manual: 1 wallclock secs ( 1.88 usr + 0.00 sys = 1.88 CPU) @ 797872.34/s (n=1500000)
moose: 6 wallclock secs ( 5.14 usr + 0.00 sys = 5.14 CPU) @ 291828.79/s (n=1500000)
Oops. If we actually try to take advantage of the features of Moose, we still take a serious performance hit. For most people will this won't matter. Ah, but you argue that I should have that type checking and you're right, but in reality, much Perl code deep in a system doesn't have type checking. But let's add a quick check of our own, just to be more fair.
sub bar {
my $self = shift;
return $self->{bar} unless @_;
croak "Need int, not ($_[0])" unless 0+$_[0] =~ /^\d+$/;
$self->{bar} = shift;
}
That's not a great check, but it's better than many people provide. Here's the benchmark:
Benchmark: timing 1500000 iterations of manual, moose...
manual: 2 wallclock secs ( 3.35 usr + 0.00 sys = 3.35 CPU) @ 447761.19/s (n=1500000)
moose: 4 wallclock secs ( 5.20 usr + 0.00 sys = 5.20 CPU) @ 288461.54/s (n=1500000)
Again, with carefully crafted code, we can outperform Moose, but we still don't get Moose's flexibility. This will matter to very few people and unless you have a very clear reason, don't skip Moose just for this. Regrettably, our millisecond response times mean that we have a problem.
That problem, in this case, is multiple inheritance. As with many codebases that evolve over time, lots of programmers have had a chance to "improve" the system and I'm seeing a lot of multiple inheritance. I'm seeing classes which have five parents! Running Class::Sniff over them is showing quite a few issues and it's clear from even a cursory examination that this MI is for sharing behavior.
Sharing behavior is exactly what roles are for. So if we're concerned about the overhead of Moose, what options do we have? I've deprecated Class::Trait. Is it time to resurrect (and benchmark) it? Mouse seemed promising, but initial benchmarks with the above code showed it's getters/setters running slightly slower than Moose getter/setters! We can take a performance hit on load, but on runtime, we have to be careful.
Maybe we need a very lightweight:
use role
'Does::Seriliazation',
'Does::TitleSearch',
'Does::IdMatching' => { excludes => 'some_method' };
No runtime application would be allowed. There would be no introspection beyond DOES. Multiple "use role" in the same class would fail (this solves a few problems I won't go into now). No Moose, Mouse or anything else would be required. Better suggestions are welcome. I'll guess that I could use Moose without accessors and without inlining constructors and take advantage of roles that way. Sounds better, but more benchmarking is needed.