Perl で配列回すときにデリファレンスしといた方がいいと勘違いしてました、、

避暑地でハッカソンがしたい。

前置きー。

JavaScript で、配列の長さを評価する必要がないとき、
以下のよう配列長をキャッシュすると思います。

配列の長さが変わらないなら、、

for ( var i = 0 ; i < arr.length; i++ ) { //do something}

配列長の変数に入れておく。

for ( var i = 0, l = arr.length; i < l; i++ ) { //do something}


それで、Perlでもおんなじ風味で、
キャッシュしておいた方がいいと思うと、やっぱり予想通りでした。

use strict;
use warnings;
use Benchmark qw/cmpthese/;
 
my @arr = 0..100;

# Benchmark js style
cmpthese(100000, {
    'js' => sub {
        my $total = 0;
        for (my $i = 0; $i < scalar @arr ; $i++) {
            $total += $arr[$i];
        }
    },
    'js cache' => sub {
        my $total = 0;
        for (my $i = 0, my $l = scalar @arr; $i < $l ; $i++) {
            $total += $arr[$i];
        }
    },
});

ベンチ。

            Rate       js js cache
js       51020/s       --     -19%
js cache 63291/s      24%       --

この後の本題のために、この例のfor文を簡素化してみてベンチ。

use strict;
use warnings;
use Benchmark qw/cmpthese/;
 
my @arr = 0..100;

# Benchmark coding style
cmpthese(100000, {
    'array' => sub {
        my $total = 0;
        $total += $_ for @arr;
    },
    'js cache' => sub {
        my $total = 0;
        for (my $i = 0, my $l = scalar @arr; $i < $l ; $i++) {
            $total += $arr[$i];
        }
    },
});

変数の代入が無い分速いのかな?と疑問は残りつつ、前置き終了。

             Rate js cache    array
js cache  66667/s       --     -39%
array    108696/s      63%       --

本題ー。

ここで予想したのが、

「配列長をキャッシュしておいた方がいいなら、
配列のデリファレンスしたものもキャッシュしておいた方がいいのかな?」

でした。

ですが、違いました以下のように配列のキャッシュを十分活かせない形だと、デリファレンスするコストの方が高くなるようです(2013-11-19修正)

具体的には、以下のベンチ。

use strict;
use warnings;
use Benchmark qw/cmpthese/;
 
my @arr = 0..100;
my $arr = \@arr;
sub get_arr { return @arr; }
sub get_arr_ref { return $arr; }

# Benchmark dereference
cmpthese(100000, {
    'array' => sub {
        my $total = 0;
        $total += $_ for @arr;
    },
    'get_array' => sub {
        my $total = 0;
        $total += $_ for get_arr();
    },
    'get_array cache?' => sub {
        my $total = 0;
        my @a = get_arr(); #キャッシュ?
        $total += $_ for @a;
    },
});


# Benchmark dereference
cmpthese(100000, {
    'array_ref' => sub {
        my $total = 0;
        $total += $_ for @$arr;
    },
    'get_array_ref' => sub {
        my $total = 0;
        $total += $_ for @{ get_arr_ref() };
    },
    'get_array_ref cache?' => sub {
        my $total = 0;
        my @a = @{ get_arr_ref() }; #キャッシュ?
        $total += $_ for @a;
    },
});

ベンチ結果。

                     Rate get_array cache?        get_array            array
get_array cache?  40161/s               --             -43%             -64%
get_array         69930/s              74%               --             -38%
array            112360/s             180%              61%               --

リファレンスとして扱った方

                         Rate get_array_ref cache?  get_array_ref      array_ref
get_array_ref cache?  49261/s                   --           -55%           -56%
get_array_ref        108696/s                 121%             --            -3%
array_ref            112360/s                 128%             3%             --
       2%    --


デリファレンスしておいたものを変数代入(キャッシュ?)しておいた方が遅かったです。

どうしてこういう結果になったのか、腹落ちしてないので、たぶん、、調べる、、、、

  • 代入のコストが意外と大きい?
  • あと意外なのが、ger_array より、get_array_ref の方が速かったこと。

蛇足。

Sledge のコードを読んでいて、登録されているトリガーをぶん回しているところを読んでいて沸いた疑問でした。
miyagawaさんが、書くなら、それがいいんだろうと思ってベンチを取ってみて、やっぱりそうでした。

追記

sort についても、、、
ループのたびに評価されるって思ってた、、、完全な勘違い、、残念すぎる、、orz


use List::Util qw/shuffle/;

my @num = shuffle 0..9;

# Benchmark dereference
cmpthese(1000000, {
    'simple' => sub {
        my $s = '';
        $s .= $_ for sort @num;
    },
    'cache?' => sub {
        my $s = '';
        my @sorted_num = sort @num;
        $s .= $_ for @sorted_num;
    },
});

ベンチ

           Rate cache? simple
cache? 128041/s     --   -58%
simple 305810/s   139%     --

追記2 2013-11-19

ベンチが十分、arrayのキャッシュを活かせる形じゃなかった。
コメントのnyaさんの指摘で気づきました。ありがとうございます><