2005年07月08日

関数呼び出しとクラス継承のベンチマーク

フレームワークを考えるにあたって、気になる部分のベンチマークを取ってみた。
ポイントは次の3点。

  1. 関数の呼び出し方法: Class::func() と Class->func() 形式
  2. クラスを継承した場合のペナルティ: Class->() と SuperClass->()
  3. 連想配列への直接アクセスと、アクセサ経由のアクセス

ベンチマーク環境: perl, v5.8.6 built for darwin-thread-multi-2level / Mac OS X 10.4 (Tiger) / Apple Computer, PowerBook G4 12", 867MHz, 640MB RAM

まず、コード1。

#!/usr/bin/perl
 
package Parent;
use strict;
use warnings;
 
sub pid {
    return $$;
}
 
package Child;
use strict;
use warnings;
use base qw(Parent);
 
package main;
use strict;
use warnings;
use Time::HiRes;
use Benchmark qw(:all :hireswallclock);
 
# Bench 1
my $results = timethese(0, {
    'Parent::pid()' => 'Parent::pid()',
    'Parent->pid()' => 'Parent->pid()',
    'Child->pid()' => 'Child->pid()',
});
print "\n";
cmpthese($results);
 
1;

結果

                  Rate  Child->pid() Parent->pid() Parent::pid()
Child->pid()  452184/s            --           -1%          -30%
Parent->pid() 455510/s            1%            --          -29%
Parent::pid() 644448/s           43%           41%            --

親クラスを継承した子クラスを経由して親クラスにある関数を呼び出すより、親クラスの関数を呼び出す方が1%速い。つまり、クラスを継承する事によるペナルティはほとんど無いと言える。
次に、Class->func() と -> 演算子を使って関数を呼び出すより、 Class::func() とパッケージ指定で直接関数を呼び出した方が 41% 速い。
これは、 -> 演算子がコンパイル時ではなく実行時に関数名を評価するためじゃないかと思う。
詳しくは「Perl の変数に関するちょっとした誤解と、動的な性質について : NDO::Weblog」が参考になる。


そして、コード2。

#!/usr/bin/perl
 
package MyAccessor;
use strict;
use warnings;
 
sub new {
    my $class = shift;
    my %value = @_;
    bless \%value, $class;
}
 
sub param {
    my $self = shift;
    unless (@_)     { return sort keys %$self }
    if     (@_ > 1) { $self->{$_[0]} = $_[1]  }
    return $self->{$_[0]};
}
 
package main;
use strict;
use warnings;
use Time::HiRes;
use Benchmark qw(:all :hireswallclock);
 
# Bench 2
my $pid = $$;
my $now = time();
my %Hash = (pid => $pid, now => $now);
my $Ref  = \%Hash;
my $Inst = MyAccessor->new(%Hash);
 
my $results = timethese(0, {
    '%Hash' => sub { $pid = $Hash{pid}; $now = $Hash{now} },
    '$Ref'  => sub { $pid = $Ref->{pid}; $now = $Ref->{now} },
    'param()' => sub { $pid = $Inst->param("pid"); $now = $Inst->param("now") },
});
print " \n";
cmpthese($results);
 
1;

結果

            Rate param()    $Ref   %Hash
param()  94371/s      --    -83%    -86%
$Ref    550459/s    483%      --    -15%
%Hash   651097/s    590%     18%      --
連想配列のリファレンスからデリファレンスしつつ値を取り出すより、普通に連想配列にアクセスする方が18%速い。 アクセサを通じて値を取り出すより、連想配列から値を取り出す方が590%速い。

とうわけで、“関連したデータの集まり”をアクセサを持ったValueObjectとして扱うよりも、連想配列か、連想配列のリファレンスとして扱う方がかなり速いという事になる。
OOPの勉強をするにはちとマズイ情報かもしれないが、実用の範囲では過度なOOP化は控えた方が良いという事になる。


ここからは余談。

MovableTypeを使い始めてから、動作の遅さに驚いた。
最初は外部ライブラリを使い過ぎているのではないかと思っていたが、ソースを見てみると、かなりOOP化が進んだ作りだったため、過度なオブジェクト化と多段継承が原因ではないかとにらんでいた。
もちろん、Catalystも徹底したOOP構造で作られているので、MovableTypeと同じような弱点を持っているだろうと予測され、自分がフレームワークを作る場合は実用性重視のものを作ろうと思っていた。

本来なら、アプリケーションより下位にあたるライブラリ関連は、オブジェクト化されて mod_perl 上で共有されるメリットはあるかもしれないが、アプリケーションの上位にあたるフレームワークは、mod_perl 上で共有される意義はあまりない(少なくとも、ライブラリほどではない)と思われる。

・・・という持論の根拠のためのベンチマーク。

Posted by SYN at 2005年07月08日 23:52 for Develop | TrackBack
Comments