XSで共有文字列を活用する

Perl 5.8以降には共有文字列というメカニズムがあり,非常に限定的ながら,うまく使用するとメモリと速度の双方を節約できる。
基本的な使い方:

SV* sv = newSVpvn_share(pv, len, hash);
SV* sv = newSVpvs_share("...");

これでsvの文字列部分がインタプリタ全体で共有されるため,同じ文字列を複数個生成してもmalloc(3)は一度で済む。

実際には,共有文字列SVの生成速度そのものは通常の文字列SVよりも遅いことがある。しかし,共有文字列の真価は,ハッシュキーとして使用する際に発揮される。
ハッシュからキーを検索する際にはキーとなる文字列の照合*1を行わなければならないが,共有文字列はポインタ値がインタプリタを通して等しいので,文字列の照合は行わなくてすむ。また,共有化文字列SVは内部にハッシュ値*2を持っているので,ハッシュ値の計算も行わなくてすむ。その結果として,hv_xxx_ent()のキーとして共有文字列を与えると,通常の文字列SVよりも高速にアクセスできる。

したがって,共有化文字列はXSレベルでハッシュにアクセスするときに使用すると効果的である。また,Perlではサブルーチン名はシンボルテーブルハッシュのキーとして保存されているので,メソッドの呼び出し時にも使える。
いくつかのPerlコードやXSコードで検証した結果,ハッシュキーとして共有文字列SVを使うと,通常の文字列SVを使用したときと比べ,約15%ほど高速になるようである*3

実用的には,たとえばClass::MOPの現行バージョン(0.83)では,いくつかのハッシュキーやメソッド名と対応するハッシュ値をあらかじめ自前で生成しているのだが,これはnewSVpvn_share()/newSVpvs_share()で生成した共有文字列SVにすれば,ハッシュ値を自前で保持する必要がなくなるだけでなく,ポインタの直接比較によってアクセスがより高速になる可能性がある。

なお,Perlレベルではさまざまなところで暗黙的に使用されているため,意識する必要はほとんどない。たとえば,メソッド名をリテラルで与えるとそのメソッド名はコンパイル時に共有文字列SVになる。

$o->foo(); # 'foo'は共有文字列SVなので高速
my $method = 'foo';
$o->$method(); # 'foo'は通常の文字列SVなので低速

メソッドをリテラルで与えると,変数で与えるより20%ほど高速であるようだ。もちろん変数の参照はそれ自体にコストが掛かるので,共有化文字列による効果はやはり15%ほどだと思われる。

以下ベンチマークスクリプト

#!perl -w
use strict;
use Benchmark qw(:all);
sub Foo::foo{
    my($class, $x) = @_;
    return $x;
}
my $method = 'foo';
my $o      = bless {}, 'Foo';
cmpthese -1 => {
    method_named => sub{
        $o->foo(42) for 1 .. 100;
    },
    method => sub{
        $o->$method(42) for 1 .. 100;
    },
};
__END__

結果(perl 5.10.0 on linux, multi-thread):

               Rate       method method_named
method       7802/s           --         -17%
method_named 9398/s          20%           --

なお,この共有化文字列を含めたPerlのCOWシステムは開発途上にあり,完成しているとはいえないようである。perlapiにあるのもnewSVpvn_share()とnewSVpvs_share()だけで,他のいつくかの関連APIは非公開となっている。ハッシュキーやメソッド名として使う分には問題はないが,それ以上の内部構造に立ち入った利用はまだ避けたほうがいいだろう。

*1:memcmp(3)による。

*2:このハッシュ値とは,コンテナとしてのハッシュではなく,ダイジェストとしての符号なし整数値である。

*3:Perl 5.10.0スレッドありの場合。Perlのバージョンや構築オプションによって変動がある。