キャッシュしよう

京都観光で散財しすぎて貯金がないmalaです。こんにちは。キャッシュの話を書きます。

色んなキャッシュがあります

簡単に思いつくのはこの程度ですが、スケーラブルなウェブサイトを構築するには常識的に考えてそんなのキャッシュしねーだろうというようなものをキャッシュする必要があります。

DateTimeをキャッシュしよう

同じ時刻に対するDateTimeオブジェクトをキャッシュします。

package MyDateTime;
use strict;
use base qw(DateTime);

my %CACHE;

sub now {
    my $class = shift;
    $class->from_epoch(epoch => (scalar time), @_);
}

sub from_epoch {
    my ($class, %args) = @_;
    my $key = "from_epoch::".$args{epoch};
    my $obj = $CACHE{$key};
    return $obj->clone if (defined $obj);

    my $self = $class->SUPER::from_epoch(%args);
    $CACHE{$key} = $self;
    return $self;
}

1;

手元のMacBook(Core Duo 2GHz/Perl 5.8.8)でベンチマークを取ってみた結果はこちら

            (warning: too few iterations for a reliable count)
              Rate        now now_cached
now         3086/s         --       -95%
now_cached 66667/s      2060%         --

現在時刻を生成しまくるという極端なケースですが、20倍ぐらい高速化することができます。 オリジナルのDateTime->nowを呼び出すと、DateTimeオブジェクトを一回生成するあたり、0.3msぐらいかかっているということがわかります。 たかが0.3msですが、Feedを解析したりしていると10000件ぐらいのDateTimeオブジェクトを作ったりすることも良くありますから、合計で3秒かかったりして、結構無視できなかったりします。

ちなみにこのコードはtimezone等、他の引数を全く考慮しておらず、かなりいい加減な代物ですから、注意してください。 cloneしているのはDateTimeオブジェクトを破壊的に使う可能性があるからです。

Method::Cachedを使って手軽に高速化

こういったコードをいちいち書くのが面倒くさいのでbonnnuさんのMethod::Cachedを試してみました。memoizeとかmemoiseとか呼ばれる奴ですね。

attributeを使ってキャッシュの有効期限と、引数のシリアライズルールを記述してやると、そのメソッドの結果がキャッシュされるようになります。

サンプルコード。

package MyDateTime2;
use strict;
use base qw(DateTime);
use Method::Cached;

Method::Cached->set_domain(
    datetime => {
        storage_class => 'Cache::FastMmap'
    }
);

sub now {
    my $class = shift;
    $class->from_epoch(epoch => (scalar time), @_);
}

sub from_epoch :Cached("datetime", 60, [SELF_SHIFT, HASH]){
    my ($class, %args) = @_;
    warn "called";
    $class->SUPER::from_epoch(%args);
}

1;

benchmark.pl

#!/usr/bin/perl

use strict;
use DateTime;
use MyDateTime;
use MyDateTime2;
use Data::Dumper;
use Benchmark qw(cmpthese);

use Cache::FastMmap;
my $a = MyDateTime2->now;

my $cache = Cache::FastMmap->new;
$cache->set(datetime => $a);
$cache->set(simple   => 1);

cmpthese (10000, {
    now => sub {DateTime->now},
    now_cached => sub {MyDateTime->now},
    now_cached2 => sub {MyDateTime2->now},
    fastmmap => sub {$cache->get("datetime")},
    fastmmap_simple => sub {$cache->get("simple")},
});

結果はこちら。

called at MyDateTime2.pm line 19.
            (warning: too few iterations for a reliable count)
            (warning: too few iterations for a reliable count)
called at MyDateTime2.pm line 19.
called at MyDateTime2.pm line 19.
called at MyDateTime2.pm line 19.
called at MyDateTime2.pm line 19.
called at MyDateTime2.pm line 19.
called at MyDateTime2.pm line 19.
called at MyDateTime2.pm line 19.
called at MyDateTime2.pm line 19.
                   Rate now_cached2 fastmmap      now fastmmap_simple now_cached
now_cached2      1553/s          --      -8%     -49%            -96%       -98%
fastmmap         1686/s          9%       --     -45%            -95%       -97%
now              3040/s         96%      80%       --            -92%       -95%
fastmmap_simple 37037/s       2285%    2096%    1119%              --       -44%
now_cached      66667/s       4193%    3853%    2093%             80%         --

残念ながらオリジナルのDateTimeよりも遅くなってしまいました。約半分の速度に落ちました。

結果的に遅くなってますが10000回呼び出されているのに、オリジナルのfrom_epochメソッドが呼び出されたのは8回ですので、同じ秒数である限りfrom_epochがキャッシュを使っているのがわかります

Cache::FastMmapからDateTimeオブジェクトを引くだけの処理で1686/sなので、キャッシュを引く処理が遅いということになります。Cache::FastMmapにシリアライズが不要なシンプルなデータをキャッシュした場合は37000/sが出ていますので、storableの速度が遅いのだと予想が付きます。元々速い処理をさらに高速化したいような場合だと、Storableの速度や関数の呼び出し速度が影響して、Cache::*系のモジュールを使っても高速化できないケースがあります。

クロージャとキャッシュ

あまり関係ないですが、キャッシュ関係のコードを書いていると「失敗したら代わりに何かする」系の処理に良く遭遇します。こういった処理はクロージャを使うと記述が楽になったりすることが多いです。

今までこのように書いていたのを

sub hoge {
    my $cache = MyCacheClass->new;
    my $cache_key = "hoge";
    if (defined (my $data = $cache->get($cache_key)){
        # HIT CACHE
        return $data;
    }
    my $result = Something->heavy();
    $cache->set($cache_key => $result, 60 * 60 * 3); # 3hours
    return $result;
}

こんな風に書くのがマイブームです。

sub hoge {
    my $cache = MyCacheClass->new;
    my $cache_key = "hoge";
    $cache->get_or_set($cache_key, sub {
        Something->heavy();
    }, 60 * 60 * 3);
}

関連してそうなCPANモジュール

次はyoupy(ブラクラ注意)さんに回したいと思います。

Back