hnwの日記

PHPのlibjitバインディングであるJIT-Fuを試してみた

@krakjoeさんが作っているPHPエクステンションのJIT-Fuを試してみました。これはGNUJITコンパイラライブラリlibjitPHPから使うためのものです。

インストール

READMEの「Installation Instructions」に書いてある通りにlibjitとJIT-Fuをインストールします。今回はMacOSX 10.9+PHP 5.6.3の環境で試しました。


まずはlibjitをインストールします。僕はHomebrewと衝突するのがイヤで/opt以下にインストールしましたが、普通は/usr/localあたりにインストールすればいいと思います。

git clone git://git.sv.gnu.org/libjit.git
cd libjit
./auto_gen.sh
./configure --prefix=/opt
make
sudo make install


次にJIT-Fuをインストールします。

git clone https://github.com/krakjoe/jitfu
cd jitfu
phpize
LDFLAGS=-L/opt/lib/x86_64 ./configure --with-jitfu=/opt
make
make install
# 環境によってはsudo make installになるでしょう


イマイチ状況がわかっていないのですが、configureがライブラリを正しく見つけられないようで、configure時にLDFLAGSを指定する必要がありました(参照:configure: error: wrong libjit version or libjit not found · Issue #7 · krakjoe/jitfu)。

遊んでみる

ではさっそく遊んでみましょう。ドキュメントはほとんどありませんが、READMEにフィボナッチ関数の例が書いてあるので、メソッド名をソースコードから拾いながらギリギリ使えるかと思います。


今回、libjitのドキュメントを参考に最大公約数を求めるプログラムをJIT-Fuで書いてみました。


まずはPHP版を書いてみます。

<?php
function gcd($x, $y)
{
    if ($x === $y) {
        return $x;
    } else if ($x < $y) {
        return gcd($x, $y - $x);
    } else {
        return gcd($x - $y, $y);
    }
}

$z = gcd(407074349875, 691666152);


引き算だけで実現したユークリッドの互除法です。なぜかlibjitドキュメント中のサンプルは再帰が多いんですよね。同じ処理をJIT-Fuで書くと次のようになります。

<?php
use JITFU\Context;
use JITFU\Type;
use JITFU\Signature;
use JITFU\Func;

$context = new Context();

$int = Type::of(Type::long);

$gcd = new Func($context, new Signature($int, [$int, $int]), function($args) {
        $x = $args[0];
        $y = $args[1];

        /* if ($x === $y) return $x; */
        $this->doIf(
            $this->doEq($x, $y),
            function() use ($x) {
                $this->doReturn($x);
            }
        );

        /* if ($x < $y) return gcd($x, $y - $x); */
        $this->doIf(
            $this->doLt($x, $y),
            function() use ($x, $y) {
                $this->doReturn(
                    $this->doCall($this, [$x, $this->doSub($y, $x)])
                );
            }
        );

        /* else return gcd($x - $y, $y); */
        $this->doReturn(
            $this->doCall($this, [$this->doSub($x, $y), $y])
        );
    });

$z = $gcd(407074349875, 691666152);


圧倒的に長くて読みにくいプログラムができあがりました。実際に動かすと期待通りに動作します。僕の環境では再帰の深さが13万ほどになるとsegmentation faultで落ちるという問題がありましたが、原因は追いかけていません。

性能

上で作った2種類のgcd計算について、何回か繰り返し呼び出して性能を比較してみました。

繰り返し回数 PHP JIT-Fu版
1 0.00153s 0.00022s
10 0.00868s 0.00045s
100 0.06693s 0.00222s


1回の実行では7倍程度、100回ループでは30倍程度JIT-Fu版の方が高速です。このように重い処理を実行したり同じ処理を繰り返し実行するような場合にはJIT-Fu導入で高速化できる可能性があると言えます。挙動から判断すると、1回目の実行タイミングでJITコンパイルを行い、以後はそのコンパイル結果を使い回すような処理になっているようです。


今回の例は関数呼び出し1回でもかなり重い処理であるため、JIT-Fuに有利な結果であることに注意してください。もっと軽い処理であればJIT-Fu版の方が遅くなってきます。これは、JITコンパイル処理の時間が一定であるため、軽い処理の場合はJIT-Fuが相対的に不利になるためです。

感想など

JIT-Fuプログラミングでは、ちょっとしたバグがあると頻繁にsegmentation faultが出ます。おそらくJIT-Fuのエラー処理が甘いせいでlibjitに不適当なパラメータを渡してしまうのでしょうが、LL言語気分で書ける割に実行時にノーヒントで落ちるのは中々エキサイティングです。現状だと実用には厳しい印象ですね。


また、僕の理解度不足かもしれませんが、現実的に扱えるデータ型が整数と浮動小数点数くらいしかないように思います。さらにJIT-Fu関数から他の関数が呼び出せない気がしますので、もしそうなら利用できる範囲はかなり限定的です。


とはいえ、これから化ける可能性が十分あるプロダクトだと思います。楽しみですね。



ちなみに上の画像はJIT-Fuの暫定(?)マスコットキャラのようですが、どうやら毎度おなじみのelephpantをニンジャ化(ninjafy)したものなんだそうです。元ネタはニンジャタートルズでしょうけど、ちょっと複雑な気持ちになりますね…。