(byte|文字列)計算系のバッチ処理と Inline::C の相性の良さ

どんなシステムにおいてもオンライン系の処理ばかりではなくバッチ処理系のプログラムというものが存在します。僕の場合、ユーザに見えない部分のプログラムが全体の70%を占めているかもしれません。

バッチ処理は大量のデータを処理する場合が多いのが特徴のうちのひとつ。しかもその処理が想定時間内に終わらないと、どこぞのシステムのようにシステム全体に影響が出てしまいシステムダウンなんて最悪の状態になることもあります。あれに起因してバッチの突き抜けなんて効いたこともない言葉が生み出されたくらいだし。w

さて話を戻して、システムを運用しているとデータ量は当然右肩上がりで増えていきます。よってバッチ処理も当時の想定よりも時間がかかるようになってくるわけです。

僕が作ってるバッチ処理の多くは文字列処理や計算処理が多いです。オンライン系の言語と同じく perl で記述しているものが非常に多いわけですが、Devel::NTYProf とか使っても最適化&高速化にも限度があるってもんです。そんな場合に心強い友が Inline::C です。

- スポンサーリンク -

オンライン系ならもちろん XS で書きますが、逆にバッチ系だと Inline::C が非常に見通しがいいと思ってます。多くの場合、バッチ処理のプログラムはそんなに複雑ではありません。しかもひとつのファイルにぺろっとプログラムが書かれている場合が多いです。僕の場合。

したがって、万が一他の人に引き継ぐことを考えても、__DATA__ 配下の __C__ に C 言語で記述された関数があって、それが perl 部分から call されているだけってのは非常に見通しがいいわけです。しかも速度は流石 C 言語で書いてるだけあって高速です。その辺は XS での効果と同じです。しかもバッチ処理なので mode_perl で永続化されるわけでもないので、メモリの扱い方をちょっとくらいミスっていても何となく安心感があります。いや、ダメだけどさ・・・。

さて先日 Inline::C 化した処理のひとつを例に挙げてみます。

0〜9 の数値列が連結された文字列をカンマ区切りの文字列に変換する処理。つまり

0123456789 → 0,1,2,3,4,5,6,7,8,9

です。このの処理が1億回くらい処理する必要があるので、わずかな高速化でも数分というオーダーで高速化されていきます。簡単なベンチマークを書いてみました。1億回は計算時間が長いので100万回で比較。
use Benchmark;
use Inline C;

my $str = '0123456789' x 1;

timethese(
    1000000,
    {   "substr" => \&use_substr,
        "split"  => \&use_split,
        "inline" => \&use_inline_c,
    }
);

sub use_substr {
    my $chr;
    my $out = '';
    for ( my $i = 0; $i < length($str); $i++ ) {
        $chr = substr( $str, $i, 1 );
        $out .= "$chr,";
    }
}

sub use_split {
    my $out = '';
    foreach my $chr ( split //, $str ) {
        $out .= "$chr,";
    }
}

sub use_inline_c {
    my $out = '';
    $out = _use_inline_c($str);
}

1;
__END__

__C__
#define MAX_LEN  1000

SV* _use_inline_c(char* src)
{
    char  tmp[10] = "";
    char  str[MAX_LEN] = "";
    int len = strlen(src);
    int cnt = 0;
    int i   = 0;

    for (i = 0; i <= len-1; i++ ) {
        str[cnt++] = *(src+i);
        str[cnt++] = ',';
    }
    str[cnt] = NULL;
    return newSVpv(str, strlen(str));
}

実行結果は以下の通り。Inline::C で計算部分を書き直した関数を call する場合は perl の split 処理yや substr 処理と比較して5倍ほど高速に処理可能でした。

Benchmark: timing 1000000 iterations of regexp, split, substr...
    inline:  1 wallclock secs ( 1.09 usr +  0.00 sys =  1.09 CPU) @ 915750.92/s (n=1000000)
     split:  6 wallclock secs ( 6.72 usr +  0.02 sys =  6.74 CPU) @ 148367.95/s (n=1000000)
    substr:  5 wallclock secs ( 5.15 usr +  0.00 sys =  5.15 CPU) @ 194250.19/s (n=1000000)

もう随分と C 言語を触っていなかったので、すんごい勢いで忘れちゃてましたが、Inline::C なら簡単にココ C 言語にしてみようかなーって思えます。

Inline::C がどんな仕組みになっているかは、まだ勉強不足で説明できないんだけど、最初に実行するときに __C__ 配下がコンパイルされ、当該プログラム直下に _Inline ってフォルダが作成され、その中でコンパイルされた実行ファイルが保存されるようです。つまりこんな感じのフォルダ階層に。

C:\Users\drk\Desktop\作業\08\12\081213>tree /F
Folder PATH listing for volume system
Volume serial number is 0024F418 60B4:42CE
C:.
│   strbench.pl
│   結果.txt
│
└───_Inline
    │   config
    │
    ├───build
    └───lib
        └───auto
            └───strbench_pl_00f8
                     strbench_pl_00f8.bs
                     strbench_pl_00f8.dll
                     strbench_pl_00f8.inl

でもってソースを改変すると新しくハッシュ値が計算され、フォルダが新しく生成される。

C:\Users\drk\Desktop\作業\08\12\081213>tree /F
Folder PATH listing for volume system
Volume serial number is 0014F7D0 60B4:42CE
C:.
│   strbench.pl
│   結果.txt
│
└───_Inline
    │   config
    │
    ├───build
    └───lib
        └───auto
            ├───strbench_pl_00f8
            │       strbench_pl_00f8.bs
            │       strbench_pl_00f8.dll
            │       strbench_pl_00f8.inl
            │
            └───strbench_pl_b103
                    strbench_pl_b103.bs
                    strbench_pl_b103.dll
                    strbench_pl_b103.inl

ちなみにコンパイルに失敗するようなプログラムだと残骸が残る。

C:\Users\drk\Desktop\作業\08\12\081213>tree /F
Folder PATH listing for volume system
Volume serial number is 0010F400 60B4:42CE
C:.
│   strbench.pl
│   結果.txt
│
└───_Inline
    │   config
    │
    ├───build
    │   └───strbench_pl_9bba
    │       │   INLINE.h
    │       │   Makefile
    │       │   Makefile.PL
    │       │   out.make
    │       │   out.Makefile_PL
    │       │   pm_to_blib
    │       │   strbench_pl_9bba.c
    │       │   strbench_pl_9bba.xs
    │       │
    │       └───blib
    │           ├───arch
    │           │   │   .exists
    │           │   │
    │           │   └───auto
    │           │       └───strbench_pl_9bba
    │           │               .exists
    │           │
    │           ├───bin
    │           │       .exists
    │           │
    │           ├───lib
    │           │   │   .exists
    │           │   │
    │           │   └───auto
    │           │       └───strbench_pl_9bba
    │           │               .exists
    │           │
    │           ├───man1
    │           │       .exists
    │           │
    │           ├───man3
    │           │       .exists
    │           │
    │           └───script
    │                   .exists
    │
    └───lib
        └───auto
            ├───strbench_pl_00f8
            │       strbench_pl_00f8.bs
            │       strbench_pl_00f8.dll
            │       strbench_pl_00f8.inl
            │
            └───strbench_pl_b103
                    strbench_pl_b103.bs
                    strbench_pl_b103.dll
                    strbench_pl_b103.inl

.xs と書かれたファイルとかあるので XS 化できるんじゃねーの?と思ったら InlineX::C2XS ってのがあるので、多分その辺でできるのではないでしょうか?

InlineX::C2XS - Convert from Inline C code to XS.

とりあえず Inlune::C に入門したばかりの僕ですが、この辺の情報を参考にガシガシやってます。

最後に Inline.pod から引用。まさにコレなわけです。

なぜInlineを使わなきゃいけないのですか?
既に2つの有名な、CでPerlを格納するためのファシリティが存在します。XSとSWIGです。両方とも、少なくともPerlに関しては、その特性は似通っています。そして両方ともInlineに比べると学習するのは非常に困難です。
XS環境を設定し利用することの学習曲線は非常に鈍いものになります。あなたは以下のドキュメントに精通する必要があります:
* perlxs
* perlxstut
* perlapi
* perlguts
* perlmod
* h2xs
* xsubpp
* ExtUtils::MakeMaker
Inlineではすぐにはじめて、実行することができます。実際にある問題に拡張することができる短いけれども完全なプログラムがたくさん入った、C Cookbookがあります。背後で行われる複雑な構築プロセスについて学習する必要はありません。あなた自身でコードをコンパイルする必要すらありません。Cのコードを書くこと以外は、Inlineがすべての面倒を見てくれます。
Perlプログラマはコンパイルのような馬鹿げたことで悩まされることはありません。"Tweak, Run, Tweak, Run"(=ちょっとイジって実行、ちょっとイジって実行)が私たちのやり方です。面白くない仕事は全て、Inlineがあなたに代わってやってくれます。
Inlineのもう1つの利点は、スクリプトで直接使うことが出来ることです。Perlのワンライナーでそれをつかうこともできます。XSやSWIGでは、常に完全に別のモジュールを設定します。1つあるいは2つの関数しか持っていなくても。Inlineは簡単なことは簡単に、難しいことも可能にします。ちょうどPerlのように。

というわけで perl で書かれたバッチ処理をちょっと高速化してみようと Inline::C をちょっと触ってみてはいかがでしょう?バイト処理、計算処理、ループ処理を C 言語化すると確実に高速になりますね。

- スポンサーリンク -