Benchmarking Perl Core Class in v5.38

John Napiorkowski - Aug 25 '23 - - Dev Community

UPDATE

Gist to my test script so you can tell me what I did wrong: https://gist.github.com/jjn1056/6a34517ec1184d0fd7190b090eec1414

Introduction

One of the biggest upsides to the idea of having improved support for objects and classes in core Perl, as opposed to one of a grab bag of options on CPAN, is the idea that since this code would be written at the C level it should be faster than options such as Moose, which is written in Perl. There are of course big upsides to having it written in Perl, mostly the fact that more programmers can contribute to it which results in something more reflective of broad community needs. Nevertheless if we could vastly reduce the cost of creating classes in Perl that could be a huge win for certain types of projects. So I created a very simple test case to compare core class with two of the most popular object systems on CPAN: Moose and Moo.

The Test Case

Since core class has very few features compared to Moo/se I choose a very simple test. The time to create a simple class with three attributes (or 'fields' as they are named now in core class) that are marked to be initialized via the 'new' constructor and then to access each of those fields. Here's what that looks like in Moose:

package MyClass::Moose;
use Moose;

has 'attribute1' => (is => 'ro');
has 'attribute2' => (is => 'ro');
has 'attribute3' => (is => 'ro');

MyClass::Moose->meta->make_immutable;
Enter fullscreen mode Exit fullscreen mode

And in Moo:

package MyClass::Moo;
use Moo;

has 'attribute1' => (is => 'ro');
has 'attribute2' => (is => 'ro');
has 'attribute3' => (is => 'ro');
Enter fullscreen mode Exit fullscreen mode

And finally in core class:

class MyClass::CoreClass {
  field $attribute1 :param;
  field $attribute2 :param;
  field $attribute3 :param;

  method attribute1() { $attribute1 }
  method attribute2() { $attribute2 }
  method attribute3() { $attribute3 }
}
Enter fullscreen mode Exit fullscreen mode

If anyone is more learned in core class and would like to suggest a better approach please let me know. Core class has to be more verbose that Moo/se since it doesn't support creating accessors for your fields (AFAIK).

And here's the initial test case:

sub test_case1 {
  my $obj = MyClass::Moose->new(
    attribute1 => 'hello',
    attribute2 => 42, 
    attribute3 => 1);
  my $attribute1 = $obj->attribute1;
  my $attribute2 = $obj->attribute2;
  my $attribute3 = $obj->attribute3;
}

sub test_case2 {
  my $obj = MyClass::Moo->new(
    attribute1 => 'hello',
    attribute2 => 42, 
    attribute3 => 1);
  my $attribute1 = $obj->attribute1;
  my $attribute2 = $obj->attribute2;
  my $attribute3 = $obj->attribute3;
}

sub test_case3 {
  my $obj = MyClass::CoreClass->new(
    attribute1 => 'hello',
    attribute2 => 42, 
    attribute3 => 1);
  my $attribute1 = $obj->attribute1;
  my $attribute2 = $obj->attribute2;
  my $attribute3 = $obj->attribute3;
}
Enter fullscreen mode Exit fullscreen mode

I used the standard Perl Benchmark class:

use Benchmark qw(:all);
timethis(2000000, \&test_case1, "Moose: Create and access");
timethis(2000000, \&test_case2, "Moo: Create and access");
timethis(2000000, \&test_case3, "Core: Create and access");
Enter fullscreen mode Exit fullscreen mode

Here's the results:

Moose: Create and access:  6 wallclock secs ( 5.55 usr +  0.02 sys =  5.57 CPU) @ 359066.43/s (n=2000000)
Moo: Create and access:  4 wallclock secs ( 3.54 usr +  0.00 sys =  3.54 CPU) @ 564971.75/s (n=2000000)
Core: Create and access:  4 wallclock secs ( 4.07 usr +  0.00 sys =  4.07 CPU) @ 491400.49/s (n=2000000)
Enter fullscreen mode Exit fullscreen mode

(I did this on my intel Mac running Perl 5.38)

Ok so... I was not expecting core class to lose out to Moo. And honestly Moose, which is widely thought of as the fat option, is not actually that much worse. Basically Moose is 35% slower than Moo and Moo is anywhere from 7-15% faster than core class after several runs. This is surprising to me. I would have expected core class, which has lower overall overhead since it has far fewer features than Moo/se AND is written in C to have utter blown the doors off its competition. So I stepped back and decided to eliminate method dispatch from the benchmarks. Basically just test object creation. So I wrote three new tests:

sub test_case4 {
  my $obj = MyClass::Moose->new(
    attribute1 => 'hello',
    attribute2 => 42, 
    attribute3 => 1);
}

sub test_case5 {
  my $obj = MyClass::Moo->new(
    attribute1 => 'hello',
    attribute2 => 42, 
    attribute3 => 1);
}

sub test_case6 {
  my $obj = MyClass::CoreClass->new(
    attribute1 => 'hello',
    attribute2 => 42, 
    attribute3 => 1);
}
Enter fullscreen mode Exit fullscreen mode

and added these to the benchmarks:

timethis(2000000, \&test_case4, "Moose: Create object");
timethis(2000000, \&test_case5, "Moo: Create object");
timethis(2000000, \&test_case6, "Core: Create object");
Enter fullscreen mode Exit fullscreen mode

And got this:

Moose: Create object:  5 wallclock secs ( 3.91 usr +  0.01 sys =  3.92 CPU) @ 510204.08/s (n=2000000)
Moo: Create object:  3 wallclock secs ( 2.71 usr +  0.00 sys =  2.71 CPU) @ 738007.38/s (n=2000000)
Core: Create object:  3 wallclock secs ( 2.72 usr +  0.00 sys =  2.72 CPU) @ 735294.12/s (n=2000000)
Enter fullscreen mode Exit fullscreen mode

Again Moose is the loser by a solid 45% (average of running this test script 10 times with pauses in between to let my computer cool off.). But again surprisingly Moo and Core Class are basically tied, even though Moo is basically Pure Perl and runs on Perl versions as far back as 5.6 (!!!) while supporting more features such as roles, type constraints and is widely compatible with many Perl object systems including the plain old bless system which has been around since the previous century. And Moose honestly isn't that much worse considering the vast overhead of its meta object protocol. I was completely expecting C/XS based Perl core class to be a magnitude faster or better in these benches. It's basically a wash.

Conclusion: Core class isn't faster

So, honestly I find this disappointing. We're being asked to give up a lot with core class and I was expecting to find some silver lining in term of performance. Especially as I was directly told one upside to using the new internal private data system was that it would be faster than blessed hashes. It's clearly not. Please feel free to offer additional tests or explanations of errors in my approach.

What about memory usage?

In theory core class written in C and using an optimized storage instead of blessed hash refs could improve memory usage. While not the most important metric for a scripting language it's worth a look. I used Devel::Size for this. Again feel free to correct me if I'm doing it wrong. Here's the examples:

use Devel::Size 'total_size';

{
  my $obj = MyClass::Moose->new(
    attribute1 => 'hello',
    attribute2 => 42, 
    attribute3 => 1);

  my $size = total_size($obj);
  print "Moose size: $size bytes\n";
}

{
  my $obj = MyClass::Moo->new(
    attribute1 => 'hello',
    attribute2 => 42, 
    attribute3 => 1);

  my $size = total_size($obj);
  print "Moo size: $size bytes\n";
}

{
  my $obj = MyClass::CoreClass->new(
    attribute1 => 'hello',
    attribute2 => 42, 
    attribute3 => 1);

  my $size = total_size($obj);
  print "Core size: $size bytes\n";
}
Enter fullscreen mode Exit fullscreen mode

And the output:

Moose size: 420 bytes
Moo size: 420 bytes
Core size: 107 bytes
Enter fullscreen mode Exit fullscreen mode

I'm actually surprised that the Moo size and Moose size are the same, I did expect Moose to be much fatter. It's possible Devel::Size is not finding the Moose Meta class or something similar. But here we do have a solid win for core class. 4x less memory usage is totally meaningful and could have a major impact for todays workloads which often run on virtualized containers and where memory usage is important. I'll be interested in seeing how this changes when or if core class plays feature catchup with Moose.

What about Plain Old Bless?

It's not a fair comparison because if you want to party like it's 1999 and just roll something directly over bless you lose tons of features and safety. But for kicks let's take a look. Here's the class:

package MyClass::Bless;

sub new {
  my $class = shift;
  return bless \%args, $class
}

sub attribute1 { return shift->{attribute1} }
sub attribute2 { return shift->{attribute2} }
sub attribute3 { return shift->{attribute3} }
Enter fullscreen mode Exit fullscreen mode

And the test results:

Bless: Create and access:  1 wallclock secs ( 0.46 usr +  0.00 sys =  0.46 CPU) @ 4347826.09/s (n=2000000)
Bless: Create object:  1 wallclock secs ( 0.48 usr +  0.00 sys =  0.48 CPU) @ 4166666.67/s (n=2000000)
Bless size: 120 bytes
Enter fullscreen mode Exit fullscreen mode

So looks like core class still wins on overall object size (bless is 11-12% larger) but the difference is much less a victory compared to Moo/se. And wow look at the speed of object creation. Old school bless is 5-6x faster than both Moo and core class. So looks like right now when you need the lightest possible objects and are able to do without the handrails Moo/se offer its worth looking at old school bless. This could matter in the case where you are making zillions of objects for something. Its a good benchmark for core class to shoot at.

One last thing

I don't hate the idea of core class. I love it. I want it to be successful. I'm very encouraged to see a solid improvement in the memory usage metric. I just want it to be better than what we already have and to reflect the needs of what programmers are actually doing.

. . . . . . . . . . . . . . .