2008年07月08日 15:30 [Edit]
perl - no accessor; # だって遅いんだもん
まずはDCONWAY先生のお言葉から。
Always use fully encapsulated objects.
オブジェクトは常にカプセル化して用いよ
これはperlの常識というより、OOの常識であるのだけど、これがperlの実装と重なることにより、perlにおいては他の言語よりも作法を破りたくなるインセンティヴが大きいのは否めない。
カプセル化のコストが、あまりに大きいのだ。
以下は、ハッシュ、ハッシュリファレンス、そしてハッシュリファレンスによるオブジェクトの性能をベンチマークしたものだ。最初が初期化、次が accessor と mutator を一度づつ呼んだ場合。(bench0.pl)いずれも「生ハッシュ」の五倍、ハッシュリファレンスの二倍半ほど遅い。
Rate hobf href hash
hobf 346092/s -- -70% -84%
href 1161878/s 236% -- -47%
hash 2198826/s 535% 89% --
Rate hobf href hash
hobf 347181/s -- -70% -84%
href 1159485/s 234% -- -46%
hash 2134227/s 515% 84% --
この遅さは何に由来するのだろうか?Perlそのものが低性能?メソッド探索に手間取っている?
事実は、意外かつ納得がいくものである。accessor が関数であるという事実そのものが主因なのだ。以下はオブジェクトの中身にどうアクセスするかをbenchmarkしたものだが、無理矢理メソッド探索をなくして関数呼び出しに手で書き換えても、9%しか高速にならない。オーバーヘッドのほとんどは、それが関数呼び出しであるということそのものにかかっている。
Rate method sub method++ sub++ direct direct++
method 430719/s -- -8% -74% -77% -81% -91%
sub 469747/s 9% -- -72% -75% -79% -90%
method++ 1678847/s 290% 257% -- -12% -25% -64%
sub++ 1901402/s 341% 305% 13% -- -16% -60%
direct 2253342/s 423% 380% 34% 19% -- -52%
direct++ 4706723/s 993% 902% 180% 148% 109% --
作法 = Best Practice は重要だが、しかし単純な accessor でも4倍から5倍というOO税はいかにも高い。何とかできないだろうか。
ここで、JavaScriptを見てみる。JavaScriptにおいて、obj.whateverは常にobj['whatever']と等価になっている。メソッドとして扱うにはobj.whatever()と必ず()を付けなければならない。
これに対して、Perlの場合においては逆にobj->whateverは常にobj->whatever()と解釈され、obj->{'whatever'}と解釈されることはない。()を節約できるのはよいのだが、逆にJavaScriptのあり方を作法として認めてしまえば、カプセル化の利点をさほど損なわずにOO税も節約できるのではないか。具体的にはobj->whateverはobj->{'whatever'}を意味することにし、obj->whatever()となっている場合にはそのままというわけである。
というわけで、やってみた。なかなか満足が行く結果が出た。
#!perl
use strict;
use warnings;
use Benchmark qw/timethese cmpthese/;
use Obj;
my $hobj = Obj->new( attr => 1 );
cmpthese(
timethese(
0,
{
explicit => sub {
$hobj->{attr} == 1 or die;
},
no => sub {
no accessor;
$hobj->attr == 1 or die;
}, # comment here
use => sub {
use accessor;
$hobj->attr == 1 or die;
},
}
)
);
Rate use no explicit
use 954355/s -- -79% -80%
no 4617898/s 384% -- -2%
explicit 4707064/s 393% 2% --
で、accessor.pmの中身はこうなっている。
package accessor;
use strict;
use warnings;
use Filter::Simple;
our $DEBUG = 1;
FILTER_ONLY code => sub {
my @lines = split /\n/, $_;
for (@lines) {
last if /use\s+accessor/;
s{ -> # arrow operator
([A-Za-z_][0-9A-Za-z_]*) # method name
(\(?) # explicitly invoked?
}{
$2 ? "->$1$2" # leave it alone
: "->{$1}" # make it a simple hash deref
}egx;
}
$_ = join "\n", @lines;
};
no warnings 'redefine';
(*import, *unimport) = (\&unimport, \&import);
1;
見てのとおりSource Filterだ。我ながらno accessor;というのがなんだか某サラ金のno loanのようで飽きれ気味なのだが、それ以上に節税効果に飽きれている。
use Foo;する方はとにかく、Foo.pm内で$self->whateverという風にaccessorを使うのは作法を通り越してoverkill(濫法)ということなのだろうか。実際「ロングテール本」の中でも、DCONWAY先生自身、モジュール内部ではaccessorを用いて$self->get_whateverとするのではなく、$whatever_of{ident $self}としていて、Inside-Out Objectにおけるプロパティを直接操作しているのはこのためなのではないか。ちなみにmy %whatever_ofなので、モジュール外からは隠蔽されている。詳しくは同書を参照のこと。
Dan the POO Monger
Source Codes
Obj.pm
package Obj;
use strict;
use warnings;
sub new { my $pkg = shift; bless {@_}, $pkg }
sub attr {
my $self = shift;
return $self->{attr} unless @_;
$self->{attr} = shift;
return $self;
}
1;
bench0.pl
#!perl
use strict;
use warnings;
use Benchmark qw/timethese cmpthese/;
use Obj;
my (%hash, $href, $hobj);
cmpthese(
timethese(
0,
{
hash => sub { %hash = ( attr => 0 ) },
href => sub { $href = { attr => 0 } },
hobf => sub { $hobj = Obj->new( attr => 0 ) },
}
)
);
cmpthese(
timethese(
0,
{
hash => sub { $hash{attr} = $hash{attr} + 1 },
href => sub { $href->{attr} = $href->{attr} + 1 },
hobf => sub { $hobj->attr( $hobj->attr + 1 ) },
}
)
);
bench1.pl
#!perl
use strict;
use warnings;
use Benchmark qw/timethese cmpthese/;
use Obj;
my $hobj = Obj->new( attr => 0 );
my $hsub = $hobj->can('attr');
sub Obj::inc_attr { shift->{attr}++ }
cmpthese(
timethese(
0,
{
method => sub { $hobj->attr( $hobj->attr + 1 ) },
'method++' => sub { $hobj->inc_attr },
sub => sub { $hsub->( $hobj => $hsub->($hobj) + 1 ) },
'sub++' => sub { Obj::inc_attr($hobj) },
'direct++' => sub { $hobj->{attr}++ },
direct => sub { $hobj->{attr} = $hobj->{attr} + 1 },
}
)
);
