bonar note

京都のエンジニア bonar の技術的なことや技術的でない日常のブログです。

Class::Accessor::Fast から Mouse へ

Class::Accessor::Fast(以下 C::A::F)を使っているクラスで、例えば Role っぽいものを表現したくなったりして、Mouse::Role 使いたい!Mouseに移行しようかな、みたいなことがあったりします。

この2つは機能的にもだいぶ違うものでもちろん簡単には比較できません。Mouse の場合には 単純にアクセサを追加するだけじゃなくて、読み書き権限の制御や方を用いたvalidation等よりきめ細かい制約を持つクラスを作ることが可能になります。

なので、その機能の代償として単純なクラスでも当然遅くなります。ある程度はしょうが ないし、 そのコストを払う価値があれば問題ないのですが、どれくらいのインパクトなのかは知っ ておく 必要があると思います。とても小さなクラスで簡単なベンチマークを取ってみました。

以下の環境で行ないました。

Mac OS X 10.6 
CPU: 2.8 GHz Intel Core 2 Duo 
4 GB 800 MHz DDR2 SDRAM 

This is perl, v5.8.9 built for darwin-2level

Class::Accessor::Fast is up to date (0.33).
Mouse is up to date (0.28).

perl のバージョンが低いので、5.10 だとまた違う結果が出るかもです。
count という値を内部に持ち、increment() というメソッドでその内部カウントを増加させるだけのクラスを作成します。以下は C::A::F の場合。

package CountupCAF; 
use strict; 
use warnings; 

use base 'Class::Accessor::Fast'; 
__PACKAGE__->mk_accessors('count'); 

sub increment { 
    my ($self) = shift; 
    my $count = $self->count; 
    $self->count(++$count); 
} 
1; 

同じものを Mouse で書きます。

package CountupMouse; 
use Mouse; 

has count => (is => 'rw', isa => 'Int'); 

__PACKAGE__->meta->make_immutable; 

sub increment { 
    my ($self) = shift; 
    my $count = $self->count; 
    $self->count(++$count); 
} 
1; 

実際には同じではなく、count には数字しか入れられないという制約が付いているなど、こちらの方が機能的です。見た目にもスッキリですね。

さらに、isa の型指定を無くしたもの。

package CountupMouseNoType; 

use Mouse; 

has count => (is => 'rw'); 

__PACKAGE__->meta->make_immutable; 

sub increment { 
    my ($self) = shift; 
    my $count = $self->count; 
    $self->count(++$count); 
} 
1; 

id:tom_lpsd 先生の指摘によると、Mouse::Meta::Method::Accessor::generate_accessor_method_inline() では

27     if ($attribute->_is_metadata eq 'rw') { 
28         $accessor .= 
29             '#line ' . __LINE__ . ' "' . __FILE__ . "\"\n" . 
30             'if (scalar(@_) >= 2) {' . "\n"; 
31 
32         my $value = '$_[1]'; 
33 
34         if ($constraint) { 
35             if ($should_coerce) { 
36                 $accessor .= 

といった感じで type_constraint の有無で作られるメソッドの内容が変わるのでこれで速度に違いが出るはず、ということでisaが無いバージョンもバリエーションに入れて見ました。

あと、意味があるかわからないですが、Role を付けたバージョン。

package CountupMouseRole; 
use Mouse; 
use Incrementable; 

has count => (is => 'rw', isa => 'Int'); 
with 'Incrementable'; 

__PACKAGE__->meta->make_immutable; 

sub increment { 
    my ($self) = shift; 
    my $count = $self->count; 
    $self->count(++$count); 
} 
1; 

Incrementable.pm

package Incrementable; 
use Mouse::Role; 
requires 'increment'; 
1; 

スピード

まずはひたすら new だけをした場合

#!/usr/bin/perl 

use strict; 
use warnings; 

use CountupCAF; 
use CountupMouse; 
use CountupMouseNoType; 
use CountupMouseRole; 
use Benchmark qw/cmpthese/; 

cmpthese(1000000, { 
    'new(CAF)'    => sub { new CountupCAF();   }, 
    'new(Mouse)'  => sub { new CountupMouse(); }, 
    'new(NoType)' => sub { new CountupMouseNoType(); }, 
    'new(Role)'   => sub { new CountupMouseRole(); }, 
}); 

結果

                Rate   new(Role) new(NoType)  new(Mouse)    new(CAF)
new(Role)   373134/s          --         -1%         -2%        -29%
new(NoType) 375940/s          1%          --         -2%        -29%
new(Mouse)  381679/s          2%          2%          --        -28%
new(CAF)    529101/s         42%         41%         39%          --

C::A::F の方が 39% 程高速ですね。思った程の違いじゃないかなという印象。余談ですが、make_immutable をしないと僕の環境では15倍くらいスピードが違うので、これは必須ですね。

5.10.1

同じスクリプトを perl 5.10.1 で実行するとまったく違う結果に

                Rate new(NoType)   new(Role)  new(Mouse)    new(CAF)
new(NoType) 369004/s          --         -1%         -2%         -4%
new(Role)   374532/s          1%          --         -0%         -3%
new(Mouse)  375940/s          2%          0%          --         -2%
new(CAF)    384615/s          4%          3%          2%          --

ほとんど差がなくなりました。。実行回数だけみると、C::A::F が遅くなってるように見えるのですがこれはなんなんだろう。。

次に、new してひたすらincrementする場合。

#!/usr/bin/perl 

use strict; 
use warnings; 

use CountupCAF; 
use CountupMouse; 
use CountupMouseNoType; 
use CountupMouseRole; 
use Benchmark qw/cmpthese/; 

my $caf    = new CountupCAF(); 
my $mouse  = new CountupMouse(); 
my $notype = new CountupMouseNoType(); 
my $role   = new CountupMouseRole(); 

cmpthese(1000000, { 
    'incre(CAF)'    => sub { $caf->increment();   }, 
    'incre(Mouse)'  => sub { $mouse->increment(); }, 
    'incre(NoType)' => sub { $notype->increment(); }, 
    'incre(Role)'   => sub { $role->increment(); }, 
}); 

printf("caf:%d\nmouse:%d\nnotype:%d\nrole:%d\n" 
    , $caf->count 
    , $mouse->count 
    , $notype->count 
    , $role->count); 

結果

                  Rate  incre(Mouse)   incre(Role)    incre(CAF) incre(NoType)
incre(Mouse)  298507/s            --           -1%          -46%          -51%
incre(Role)   302115/s            1%            --          -45%          -50%
incre(CAF)    549451/s           84%           82%            --          -10%
incre(NoType) 609756/s          104%          102%           11%            --
caf:1000000
mouse:1000000
notype:1000000
role:1000000

Mouse よりも C::A::F の方が 84% 高速。まあそうですよねという感じですね。この辺りは人によって感じかたが違うかもしれません。特筆すべきは、type_constraints の無い Mouse の方がC::A::F よりもわずかに高速だという部分です。このチェックがあるか無いかで大分速度が変わりますね。

5.10.1

微妙に差が開いた印象

                  Rate  incre(Mouse)   incre(Role)    incre(CAF) incre(NoType)
incre(Mouse)  260417/s            --           -2%          -54%          -56%
incre(Role)   264550/s            2%            --          -54%          -55%
incre(CAF)    571429/s          119%          116%            --           -3%
incre(NoType) 591716/s          127%          124%            4%            --
caf:1000000
mouse:1000000
notype:1000000
role:1000000

caf.pl

と、ここまでやったところで、Mouseのpackageの中に author/benchmarks/caf.pl というベンチマークスクリプトが入っている事が発覚。完全に徒労だった予感。。実行してみるとこんな感じです。

perl 5.8.9

bash-3.2$ perl author/benchmarks/caf.pl 
-- new
          Rate mouse   caf
mouse 371158/s    --  -27%
caf   508970/s   37%    --
-- setter
           Rate   caf mouse
caf   1747627/s    --  -11%
mouse 1960478/s   12%    --
-- getter
           Rate mouse   caf
mouse 2123852/s    --  -11%
caf   2383127/s   12%    --

perl 5.10.1

-- new
          Rate   caf mouse
caf   367589/s    --   -1%
mouse 371359/s    1%    --
-- setter
           Rate   caf mouse
caf   1818773/s    --   -7%
mouse 1946613/s    7%    --
-- getter
           Rate mouse   caf
mouse 2123852/s    --   -9%
caf   2338582/s   10%    --

ここでも Class::Accessor::Fast の new が遅くなって差が縮まっている印象。
このスクリプト中の mouse は type の無いもの(上の例で言うと CountupMouseNoType)です。

{
    package Bench::Mouse;
    use Mouse;
    has 'a' => ( is => 'rw' );
    no Mouse;
    __PACKAGE__->meta->make_immutable;
}

メモリ使用量

new して 1000000回 内部変数をincrementするだけの動作で、メモリの消費量を計ってみました。以下が計測スクリプト。

#!/usr/bin/perl 

use strict; 
use warnings; 

use GTop; 
use UNIVERSAL::require; 

my $gtop = GTop->new(); 
my $base_mem = $gtop->proc_mem($$)->size(); 

my $package = shift; 
$package->require 
    or die "cannot import package:$package"; 

my $incrementer = $package->new(); 
for (1..1000000) { 
    $incrementer->increment(); 
    if (0 == ($_ % 300000)) { 
        my $procmem = $gtop->proc_mem($$)->size() - $base_mem; 
        printf "mem:%s (%d)\n" 
            , GTop::size_string($procmem), $procmem; 
    } 
} 
printf("%d\n", $incrementer->count()); 

結果

$ perl gtop.pl CountupCAF 
mem: 264k (270336) 
mem: 264k (270336) 
mem: 264k (270336) 
1000000 
$ perl gtop.pl CountupMouse 
mem: 3.4M (3604480) 
mem: 3.4M (3604480) 
mem: 3.4M (3604480) 
1000000 
$ perl gtop.pl CountupMouseNoType 
mem: 3.4M (3604480) 
mem: 3.4M (3604480) 
mem: 3.4M (3604480) 
$ perl gtop.pl CountupMouseRole 
mem: 3.8M (4009984) 
mem: 3.8M (4009984) 
mem: 3.8M (4009984) 
1000000 

C::A::F の圧勝ですが、Mouse もそんなにめちゃくちゃメモリ食ってるって感じでも無いですね。もうちょっと複雑なケースでの検証が必要かもしれません。

まとめ

やってるうちに、これ比べること自体が無駄なんじゃないかと思えて来ました。。違うものですしね。今回の様に単純なものであれば C::A::F の方が有利なのは間違いないし、複雑なものだとC::A::F でそもそも出来なかったり。

ただし、Mouse でも型指定を行なわず C::A::F と同じ様な使い方をするのであれば、メモリ使用量はさておき少なくとも実行速度に関しては負けてないことがわかりました。実行時間やメモリ使用量に関しては、使う人の環境によって感じ方違うかと思いますが、個人的には Mouse(or Moose)の簡素な書式と型/Role等の強力なパワーを得る代償としては受け入れられる範疇かなと思いました。

本題とは関係ないですが、Class::Accessor::Fast が 5.8.9 と 5.10.1 でパフォーマンスが違うっぽい件が気になる。