北海道苫小牧市出身の初老PGが書くブログ

永遠のプログラマを夢見る、苫小牧市出身のおじさんのちらしの裏

rust の perl-xs を触る

rust の perl-xs なるリポジトリを見つけたので触ってみた。 Perlcarton と rust の cargo が動く環境1であれば、 README に書かれている通りリポジトリを clone してきて以下で簡単に試せる。

$ carton install
$ carton exec -- 'cd t && perl Makefile.PL && make test'

perl-xsPerl API を rust から使いやすいようにラップしたものという位置づけになる。 Perl API への低レベルなバインディングperl-sys で提供され、 Rust のコードのビルドは Module::Install::Rust で提供される。また、 perl-sys で C のマクロに相当する処理の rust のバインディングを提供するために、内部で Ouroboros を使っている。

perl-xs を使うと、 XS や C を書かなくても rust だけで Perl のモジュールが書ける。ただし、このクレートは発展途上であり、足りない処理も多い。例えば、今日現在のバージョンでは Perlvalues に相当する処理がないので、作ってみる。

src/hash.rs に以下のようにイテレータを実装する。

impl HV {
    ...

    #[inline]
    pub fn iter_values(&self) -> IterHVVal {
        IterHVVal::new(self)
    }

    ...
}

pub struct IterHVVal<'a>(&'a HV);

impl<'a> IterHVVal<'a> {
    fn new(hv: &'a HV) -> Self {
        unsafe { hv.pthx().hv_iterinit(hv.as_ptr()) };
        IterHVVal(hv)
    }
}

impl<'a> Iterator for IterHVVal<'a> {
    type Item = SV;

    fn next(&mut self) -> Option<Self::Item> {
        unsafe {
            let hv = &self.0;
            let pthx = hv.pthx();
            let hv_ptr = hv.as_ptr();
            let he = pthx.hv_iternext(hv_ptr);
            if he.is_null() {
                None
            } else {
                let sv = pthx.hv_iterval(hv_ptr, he);
                Some(SV::from_sv(pthx, sv))
            }
        }
    }
}

そして、これを利用して Perl のモジュールを書く。以下のような lib.rs を書いた。ハッシュの値の二乗の和を取るだけのシンプルな関数である。

#[macro_use]
extern crate perl_xs;
#[macro_use]
extern crate perl_sys;

mod xstest {
    use perl_xs::raw::NV;
    use perl_xs::HV;

    xs! {
        package XSTest;

        sub sum_values(ctx, hv: HV) {
            let n: NV = hv.iter_values().map(|sv| {
                let n = sv.nv();
                n * n
            }).sum();
            ctx.new_sv(n)
        }
    }
}

xs! {
    bootstrap boot_XSTest;
    use xstest;
}

そして、 t/ ディレクトリを参考に Makefile.PLlib/XSTest.pm を置けばそれだけで完成。なんともお手軽である。

$ carton exec -- 'cd my_example && perl Makefile.PL && make && perl -Mblib -E "use XSTest; say XSTest::sum_values({a => 1.0, b => 2.0, c => 3.0})"'

...

14

さて、 perl-xs クレートはこのように大変お手軽でいいのだが、 作者のスライド でも触れられているように残念ながらパフォーマンスに問題がある。手元でもベンチマークを取ってみたが、先程の sum_values は Pure Perl と比べて 30% しか性能を改善できなかった 2 。同じ処理を素の XS で書いたところ 350% 性能が改善したので、ほんとに遅いとしか言いようがない。

原因を探るべく、 perl-sys 側のコードを見てみると、 PerlAPIJMPENV_* でラップされていた。以下はビルド中に自動生成された perl_sys.c からの抜粋である。

int perl_sys_hv_iternext(HE** RETVAL, HV * hv) {
    int rc = 0;
    dJMPENV;
    JMPENV_PUSH(rc);
    if (rc == 0) { *RETVAL = hv_iternext(hv); }
    JMPENV_POP;
    return rc;
}

これらを取り除いたところ、 Pure Perl と比べて 100% 性能が改善するようになった。他、余計なコピー処理などを省いて 200% 程度まで改善できたが、キリがないのでそこで辞めておいた。徹底的に省けば、素の XS と同程度まで改善できるだろう。

なお、イテレータloop に変えると 20% 程度性能が改善したが、この程度の違いで済むのはさすが rust だなと思った。


  1. WSLで試した。

  2. 驚くべきことに、要素数を 10 万個まで増やすと Pure Perl に負けてしまった。