DEV Community

Toby Inkster
Toby Inkster

Posted on • Originally published at toby.ink

Marlin Racing

When I first introduced Marlin, it seemed the only OO framework which could beat its constructor in speed was the one generated by the new Perl core class keyword. Which seems fair, as that’s implemented in C and is tightly integrated with the Perl interpreter. However, I’m pleased to say that Marlin’s constructors are now faster.

(Though also I forgot to include Mouse in previous benchmarks, so I’ve rectified that now.)

         Rate  Plain   Tiny    Moo  Moose   Core Marlin  Mouse
Plain  1357/s     --    -1%   -48%   -55%   -73%   -77%   -78%
Tiny   1374/s     1%     --   -48%   -54%   -72%   -77%   -78%
Moo    2617/s    93%    91%     --   -13%   -47%   -56%   -58%
Moose  3001/s   121%   118%    15%     --   -39%   -50%   -52%
Core   4943/s   264%   260%    89%    65%     --   -17%   -21%
Marlin 5976/s   340%   335%   128%    99%    21%     --    -4%
Mouse  6237/s   359%   354%   138%   108%    26%     4%     --
Enter fullscreen mode Exit fullscreen mode

The main way I’ve squeezed out a bit of improved performance is by improving how Class::XSConstructor keeps its metadata.

Previously, if you called Local::Person->new(), the XS constructor would look up the list of supported attributes for the class in @Local::Person::__XSCON_HAS and loop through that array to initialize each attribute like "name", "age", etc. If the attribute had a type constraint, it would need to fetch the coderef to validate the value from $Local::Person::__XSCON_ISA{"name"}, and so on. All these involved looking things up in the class’s stash, which isn’t exactly slow when done via XS, but could be faster.

I’ve changed it so that the first time the constructor is called, the XS code pulls together all the data it needs into C structs.

typedef struct {
    char   *name;
    I32     flags;
    char   *init_arg;
    char  **aliases;
    I32     num_aliases;
    SV     *default_sv;
    SV     *trigger_sv;
    CV     *check_cv;
    CV     *coercion_cv;
} xscon_param_t;

typedef struct {
    char   *package;
    bool    is_placeholder;
    xscon_param_t *params;
    I32     num_params;
    CV    **build_methods;
    I32     num_build_methods;
    bool    strict_params;
    char  **allow;
    I32     num_allow;
} xscon_constructor_t;
Enter fullscreen mode Exit fullscreen mode

Rather than having to deal with attribute names being Perl SVs, they’re just simple C strings (char*).

The flags field does a lot of heavy lifting. It is a bit field with booleans indicating whether an attribute is required or optional, whether it should be a weaken reference, and other features. A lot of common defaults (attributes which default to common values like undef, true, false, 0, 1, the empty string, an empty arrayref, or an empty hashref) and common type constraints (Str, Num, Int, ArrayRef, etc) are also encoded into the flags field, so the constructor can often skip even having to look at default_sv and check_cv.

At the same time, the number of features Class::XSConstructor supports has increased, so Marlin now never needs to fall back to generating Pure Perl constructors. (The code for generating Perl constructors has now been deleted!)

A second trick is one I learned from Mouse in how it implements its strict constructor check. As a reminder, a strict constructor check is like the ones implemented by MooseX::StrictConstructor, MooX::StrictConstructor, and MouseX::StrictConstructor, along these lines:

sub new {
  # Unpack @_
  my $class = shift;
  my %args  = ( @_ == 1 and ref($_[0]) eq 'HASH' ) ? %{+shift} : @_;

  # Create new object
  my $object = bless( {}, $class );

  # Initialize each attribute
  if ( exists $args{name} ) {
    $object->{name} = $args{name};
  }
  if ( exists $args{date} ) {
    $object->{date} = $args{date};
  }

  # Strict constructor check
  for my $key ( %args ) {
    die "Unrecognized key: $key" unless $key =~ /^(name|date)$/;
  }

  return $object;
}
Enter fullscreen mode Exit fullscreen mode

Strict constructors are a really useful feature as a protection against mistyped attributes. But they do come with a speed penalty, which I guess is why Moose and Moo don’t have this feature built in. (Mouse does actually have the feature built in, but requires an extension (MouseX::StrictConstructor) to toggle it on.)

Mouse’s strict constructor check has virtually zero performance impact. I took a look at the source code to figure out how, and it is pretty smart. It just counts the number of arguments the constructor has used to initialize attributes, and only bothers with the strict constructor check if the total number of arguments is greater than that. Something like this:

sub new {
  # Unpack @_
  my $class = shift;
  my %args  = ( @_ == 1 and ref($_[0]) eq 'HASH' ) ? %{+shift} : @_;

  # Create new object
  my $object = bless( {}, $class );
  my $used_keys = 0;

  # Initialize each attribute
  if ( exists $args{name} ) {
    $object->{name} = $args{name};
    $used_keys++;
  }
  if ( exists $args{date} ) {
    $object->{date} = $args{date};
    $used_keys++;
  }

  # Strict constructor check
  if ( keys(%args) > $used_keys ) {
    for my $key ( %args ) {
      die "Unrecognized key: $key" unless $key =~ /^(name|date)$/;
    }
  }

  return $object;
}
Enter fullscreen mode Exit fullscreen mode

Genius!

With these changes, Marlin is now significantly faster than the Perl core class keyword.

Mouse still has around 10% faster accessors than Marlin, which I think might be largely down to having an integrated type system allowing pure C function calls for type constraints instead of needing to use call_sv to call an XS or Perl type check function.

Marlin does however beat Mouse significantly (around 70% faster) when it comes to delegated methods. Things like:

use v5.36;

package API_Client {
  use Marlin
    -modifiers,
    _log => {
      isa          => 'ArrayRef[HashRef]',
      default      => [],
      handles_via  => 'Array',
      handles      => {
        add_to_log   => 'push',
        responses    => 'all',
      },
    },
    ua => {
      isa          => 'HTTP::Tiny',
      default      => sub { HTTP::Tiny->new },
      handles      => {
        http_get     => 'get',
        http_post    => 'post',
      },
    };

    around 'http_get', 'http_post' => sub ( $next, $self, @args ) {
      my $response = $self->$next( @args );
      $self->add_to_log( $response );
      return $response;
    };
}

my $client = API_Client->new;
$client->http_get( ... );
$client->http_get( ... );
$client->http_get( ... );
my @responses = $client->responses;
Enter fullscreen mode Exit fullscreen mode

Marlin outperforms all other OO frameworks in this kind of method.

If you want a fast, concise OO framework, consider using Marlin.

Top comments (0)