2013/01/12

生のハッシュリファレンスを return したら遅い

ハッシュリファレンスは畳み込み的に定数扱いじゃなかったのね、ってのに気づいて驚いた(というのが本当に遅い原因か確定させて無いけど)(【追記2】参照)。

#!/usr/bin/perl
use strict;
use warnings;

use Benchmark qw/timethese cmpthese/;

my $HASH = {
    foo => 123,
    bar => 456,
    baz => 789,
};

my $result = timethese (750_000, {
    'VAL' => '&logic1;',
    'RAW' => '&logic2;',
});

cmpthese $result;

sub  logic1 { _logic1()->{foo}; }
sub _logic1 {
    return $HASH;
}

sub  logic2 { _logic2()->{foo}; }
sub _logic2 {
    return {
        foo => 123,
        bar => 456,
        baz => 789,
    };
}

#             Rate  RAW  VAL
#    RAW  590551/s   -- -58%
#    VAL 1415094/s 140%   --

【追記】

@lestrrat さんが以下のエントリで詳しく説明してくれました。

Perlでconstant foldingされるのは文字列・数値リテラルか、定数扱いできる関数だけです。
定数扱いできる関数というのは実は決まっていて、以下の条件がそろわないとconstant folding されない:
 
1. その関数は 引数を取らない、とprototypeで明示的に宣言してある
2. その関数は文字列・数値リテラルを返し、それ以外の処理を行わない

リファレンスも「constant プラグマを使えばオッケー!」だそうです。確かに、constant プラグマ版でベンチとってみると速度が戻りました(CON が constatn)。

#            Rate  RAW  CON  VAL
#   RAW  543478/s   -- -60% -61%
#   CON 1363636/s 151%   --  -2%
#   VAL 1388889/s 156%   2%   --

なるほど!

#!/usr/bin/perl
use strict;
use warnings;

use constant const_hash => {
    foo => 123,
    bar => 456,
    baz => 789
};

use Benchmark qw/timethese cmpthese/;

my $HASH = {
    foo => 123,
    bar => 456,
    baz => 789,
};

my $result = timethese (750_000, {
    'VAL' => '&logic1;',
    'RAW' => '&logic2;',
    'CON' => '&logic3;',
});

cmpthese $result;

sub  logic1 { _logic1()->{foo}; }
sub _logic1 {
    return $HASH;
}

sub  logic2 { _logic2()->{foo}; }
sub _logic2 {
    return {
        foo => 123,
        bar => 456,
        baz => 789,
    };
}

sub  logic3 { _logic3()->{foo}; }
sub _logic3 {
    return const_hash;
}

【追記2】

遅さの原因について、検証方法をブクマで教えていただきました。

Craftworks _logic2 の方は毎回メモリ確保する分遅いです。返ってきたリファレンスのアドレス確認すると毎回違うはずです。この違い意識してないとリファレンスの中身書き換えるときに意図した動作と違ってハマります。

ああ、なるほど、確かにそうだ。

というわけで、試してみる。

$ perl -le 'print foo() for (1..5); sub foo { return +{ bar => 123, baz => 456 } }'
HASH(0x86d3d3c)
HASH(0x86d4660)
HASH(0x86d3d6c)
HASH(0x86d3d3c)
HASH(0x86d4660)

アドレス自体は再利用されてるっぽい。

$ perl -le 'print foo() for (1..10000); sub foo { return +{ bar => 123, baz => 456 } }' | sort | uniq -c | sort -r
   3334 HASH(0x93a2d3c)
   3333 HASH(0x93a3660)
   3333 HASH(0x93a2d6c)

おそらく、参照の中身は毎回定義されてて、たまたまこのワンライナーの場合は3つのアドレスを使いまわして割り当てされてるんでしょうね。

まあとりあえず、return +{}; な書き方はしない方がいい、と。

サイト内検索