perlXSでSTLのstd::mapを使ってみる
ここのところC++でコードを書いているんですが、やっぱりそいつをperlから使いたい。
ということでXSについてお勉強中です。
ごく簡単なものなら書けるようになってきましたが「perlから渡したハッシュをC++側でstd::mapとして受け取りたい」といった特殊なケースではまってしまったのでメモっておきます。
いろいろ悩みはしましたが、結論から言うと「hollyなblog」さんのところでまさにドンピシャな記事を書いてくれていたので、これを参考に頑張ってみました。
以下、サンプルコードと実践手順です。
C++コード
hashを渡してstd::mapを返すというケースを想定しているので、以下のようなクラスを準備しました。コサイン類似度を計算するコードです。
vector_tool.hとして以下を用意します。
#include <iostream> #include <map> #include <string> #include <cmath> typedef std::map<std::string, double> vec; typedef vec::iterator VecIt; class VectorTool { public: VectorTool() { } ~VectorTool() { } double cosine_similarity( vec &vector_1, vec &vector_2){ double inner_product = 0.0; for(vec::iterator itr = vector_1.begin(); itr != vector_1.end(); ++itr){ if(double value_2 = vector_2[itr->first]){ inner_product += itr->second * value_2; } } double norm_1 = 0.0; for(vec::iterator itr = vector_1.begin(); itr != vector_1.end(); ++itr){ norm_1 += pow(itr->second, 2); } norm_1 = sqrt(norm_1); double norm_2 = 0.0; for(vec::iterator itr = vector_2.begin(); itr != vector_2.end(); ++itr){ norm_2 += pow(itr->second, 2); } norm_2 = sqrt(norm_2); if(norm_1 && norm_2){ return inner_product / (norm_1 * norm_2); } else{ return 0.0; } } vec unit_length(vec &vector) { double norm = VectorTool::norm(vector); for(vec::iterator itr = vector.begin(); itr != vector.end(); ++itr){ vector[itr->first] = itr->second / norm; } return vector; } double norm(vec &vector){ double norm; for (vec::iterator itr = vector.begin(); itr != vector.end(); ++itr) { norm += pow(itr->second, 2); } norm = sqrt(norm); return norm; } private: };
コサイン類似度を計算するメソッド以外にもちょろちょろとメソッドがありますが、正直ここら辺はどうでもいいです。
ポイントとしては、このコードではベクトルをstd::mapのデータ構造で取り扱っているというところになります。
perlからの呼び出しイメージ
test.plとして以下を用意します。
use strict; use warnings; use VectorTool::XS; my $vec_1 = { abc => 123.45, bcd => 234.56, cde => 345.67 }; my $vec_2 = { abc => 123.45, bcd => 234.56, cde => 345.67 }; my $tool = VectorTool::XS->new; #--コサイン類似度 my $ret = $tool->cosine_similarity( $vec_1, $vec_2 ); print "SIM : ", $ret, "\n"; #-- ノルム(正規化前) print "NORM: ", $tool->norm($vec_1), "\n"; #-- 単位長による正規化 $vec_1 = $tool->unit_length($vec_1); #-- ノルム(正規化後) print "NORM: ", $tool->norm($vec_1), "\n";
VectorTool::XSというのが今回つくるパッケージのつもりです。
ベクトルは単純なハッシュリファレンスとしています。
ベクトルの中身の値は、実験的にはどうでもよかったので、$vec_1と$vec_2で同じものにしています。
スクリプト叩くと結果はこんな風になります。(記事の内容的にはこの結果自体はどうでもいいです)
godzilla:VectorTool-XS miki$ perl -Mblib test.pl SIM : 1 NORM: 435.59849058508 NORM: 1
typemap
typemapとはperlのデータ型とC/C++のデータ型を変換するためのルール記述です。
デフォルトで用意されているもの以外にも自分で定義できるので、イケてるXS使いはみな自前でtypemapを書くみたいです。
ちなみにこのtypemapは全面的にhollyなblogさんからのコピペです。
T_VEC sv_setref_pv($arg, CLASS, (void *)$var); TYPEMAP vec T_STRING_MAP INPUT T_STRING_MAP { HV *hv; HE *he; vec t_sm; if(SvROK($arg) && SvTYPE(SvRV($arg)) == SVt_PVHV) { hv = (HV *)SvRV($arg); if(hv_iterinit(hv) == 0) { warn(\"${Package}::$func_name() -- $var is empty hash reference\"); XSRETURN_UNDEF; } } else { warn(\"${Package}::$func_name() -- $var is not a hash reference\"); XSRETURN_UNDEF; } while((he = hv_iternext(hv)) != NULL) { SV *svkey = HeSVKEY_force(he); SV *svval = HeVAL(he); //SV *svkey = hv_iterkeysv(he); //SV *svval = hv_iterval(hv, he); t_sm.insert(vec::value_type(std::string(SvPV_nolen(svkey)), SvNV(svval))); } $var = t_sm; } OUTPUT T_STRING_MAP { if($var.empty()){ warn(\"${Package}::$func_name() -- map is empty\"); XSRETURN_UNDEF; } HV *hv = (HV *)sv_2mortal((SV *)newHV()); for(VecIt it = $var.begin(); it != $var.end(); it++) { hv_store(hv, (it->first).c_str(), (it->first).size(), newSVnv(it->second), 0); } SvSetSV($arg, newRV_noinc((SV *)hv)); }
いろいろと呪文のようなコードがつらなっておりますが、
前半部分はオブジェクトを作る部分(コンストラクタ)のデータ構造の変換についての記述です。
これは以下の記事のまんまです。
perlxs入門その3 http://blog.livedoor.jp/kurt0027/archives/51850105.html
後半部分はperlのハッシュとSTLのstd::mapを変換する記述です。これもhollyさんの記事から拝借しました。
perlxs入門その5 http://blog.livedoor.jp/kurt0027/archives/51855521.html
ただしhollyさんの例だとstd::map
それにしてもtypemap、まさに呪文ですね。。まぁ読んでいけばなんとなく想像はつきますが、こんなの素で書けっていわれてもちょっと無理ですね。口から泡が出そうです。
XSコード
さてさて、ようやくXSです。
typemapがんばったおかげでXS部分はとてもシンプルです。
#include "vector_tool.h" #include "EXTERN.h" #include "perl.h" #include "XSUB.h" #include "ppport.h" MODULE = VectorTool::XS PACKAGE = VectorTool::XS VectorTool * VectorTool::new() double VectorTool::cosine_similarity(vec vector_1, vec vector_2) vec VectorTool::unit_length(vec vector) double VectorTool::norm(vec vector)
Makefile.PL
おっと忘れてはいけないMakefile.PLです。
use 5.010000; use ExtUtils::MakeMaker; # See lib/ExtUtils/MakeMaker.pm for details of how to influence # the contents of the Makefile that is written. WriteMakefile( NAME => 'VectorTool::XS', VERSION_FROM => 'lib/VectorTool/XS.pm', # finds $VERSION PREREQ_PM => {}, # e.g., Module::Name => 1.1 ($] >= 5.005 ? ## Add these new keywords supported since 5.005 (ABSTRACT_FROM => 'lib/VectorTool/XS.pm', # retrieve abstract from module AUTHOR => 'miki <miki@apple.com>') : ()), LIBS => [''], # e.g., '-lm' DEFINE => '', # e.g., '-DHAVE_SOMETHING' INC => '-I.', # e.g., '-I. -I/usr/include/other' # Un-comment this if you add C files to link with later: OBJECT => '$(O_FILES)', # link all the C files too CC => "g++", LD => "g++", XSOPT => '-C++' );
ほぼh2xsで生成されたままですが、下のほうにあるCC,LD,XSOPTは後から追記しています。
あとCやC++のファイルを別にいくつか用意してオブジェクトファイルを生成するような場合はOBJECT => '$(O_FILES)'は必要になります。これで勝手にオブジェクトファイルをリンクしてくれるようになります。
ベンチマーク
さて、こいつをmakeすると、一応期待通りに動作するモジュールが生成されたました。やったぜ!
ですが、速度の方はあまり速くありません。
同じようなことをするperlコードを別モジュールとして書いておいて、それとのベンチマークをとってみました。
use strict; use warnings; use VectorTool::XS; use VectorTool::PurePerl; use Benchmark qw(timethese cmpthese); # ベクトルは適当 my $vec_1 = { abc => 123.45, bcd => 234.56, cde => 345.67 }; my $vec_2 = { abc => 123.45, bcd => 234.56, cde => 345.67 }; # XS版とPurePerl版 my $tool_xs = VectorTool::XS->new; my $tool_pp = VectorTool::PurePerl->new; my $loop = 1000000; my $r = timethese( $loop, { pp => \&pp, xs => \&xs, } ); cmpthese $r; sub pp { my $ret = $tool_pp->cosine_similarity( $vec_1, $vec_2 ); } sub xs { my $ret = $tool_xs->cosine_similarity( $vec_1, $vec_2 ); }
結果はこうです。
godzilla:VectorTool-XS miki$ perl -Mblib bench.pl Benchmark: timing 1000000 iterations of pp, xs... pp: 7 wallclock secs ( 6.89 usr + 0.00 sys = 6.89 CPU) @ 145137.88/s (n=1000000) xs: 9 wallclock secs (10.16 usr + 0.00 sys = 10.16 CPU) @ 98425.20/s (n=1000000) Rate xs pp xs 98425/s -- -32% pp 145138/s 47% --
「XSにしたのにpure perlより遅いとはなにごとだ!」としばし怒ってみましたが、よく考えれば当然かもしれません。
コサイン類似度の計算のようにシンプルな処理の場合、その部分での言語間でのパフォーマンス差よりも、むしろtypemapのような複雑な型変換処理の方がコストが高くついてるのではないでしょうか。
つまり、もっと演算に時間がかかるような処理であればはっきりとXSが強いはず。
なのでC++とperlのモジュール側で各々100万回ループするようにしてみて、ベンチマークスクリプトからは1回だけ呼び出すように構成を変えてみました。
まったく馬鹿らしい変更ですが、わざと明示的にXSを勝たせるための変更です。
その結果がこれ。
godzilla:VectorTool-XS miki$ perl -Mblib bench.pl Benchmark: timing 1 iterations of pp, xs... pp: 5 wallclock secs ( 4.98 usr + 0.01 sys = 4.99 CPU) @ 0.20/s (n=1) (warning: too few iterations for a reliable count) xs: 0 wallclock secs ( 0.22 usr + 0.00 sys = 0.22 CPU) @ 4.55/s (n=1) (warning: too few iterations for a reliable count) s/iter pp xs pp 4.99 -- -96% xs 0.220 2168% --
おお、C++強し!強すぎる!
わざとらしいベンチではありますが、100万ループをC++側に持ってくることで圧倒的なパフォーマンスの差となりました。