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を前提としていましたが、今回はstd::mapに変更して使っています。SvNVとかnewSVnvとか、思いっきり手探り状態なので適切なのかどうかは不明です。

それにしても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)

C++のコードはvector_tool.hにまとめてあるので、それをインクルードするだけでOKです。すてき。

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++側に持ってくることで圧倒的なパフォーマンスの差となりました。

まとめ

XSでstd::mapを使う方法、hollyさんのおかげでよくわかりました。

で教訓として得たことは

  • 簡単な処理をXSにしても型変換でのコストの方が高くつくようであれば逆効果
  • 純粋に「大量ループでの演算処理スピード」を比較するとC++は圧倒的
  • やっぱりXSは変態的。でも、なんだかちょっと楽しい♪

といったところになります。

自分はよくデータ解析のようなことをしているので「ごっつい処理部分はC++で書いて、perlで色々な処理と組み合わせる」というスタイルが理想的かも。