そんなこと無いわけで。
さて、PHPの高速化といえば、
- コード最適化
- Accelerator入れる
- PHP Extensionを作る
くらいだと思いますが、今回は「PHP Extension」を使いたいと思います。
なお、この記事で登場するPHPのバージョンは、5.3.3です。
SWIGを使う
素でPHP Extensionの作成となると、覚えることが多くて結構大変なので、「SWIG」というものを利用します。
これは、C/C++のプログラムをPHPを含めたインタプリタ言語やJava、C#などで半自動的に使えるようにしてくれるというものです。便利ですね。
「.i」というSWIGに向けた処理内容を示した「インターフェースファイル」というものを作成する必要がありますが、一度作ればあるC/C++のプログラムをPHPでも、Javaでもコマンド一発(…数発くらい)で利用できるようにしてくれます。なので、SWIGでの書き方さえ覚えればかなり楽できます。
ところで、SWIGというもの、Interfaceの2012年12月号に載っていて知ったんですが、調べてみると結構有名みたいで…。
Interfaceのほうでは「ネットワークからGPIO操作したいけど、Cだとネットワーク周り大変だなー、Rubyみたいな言語だとGPIO遅いなーなんとかならないかなー」ということで、CとRubyの連携を行なっていました。興味があれば読めばいいと思います。もう無いかも知れないけど。
さて、SWIGの利用方法ですが、インストールが完了したという前提で書いていきます。
(少なくともScientific Linuxでは)yumで入ったりするんですが、バージョンが古いので公式サイトから最新のものをダウンロードしましょう。私の環境では「pcre-devel入れろよー」と言われましたが、それ以外はすんなり入りました。バージョンは「2.0.9」です。
今回は例として、「ISBN10とISBN13の相互変換」を行うことにします。はい、続き設定。
ISBN10とISBN13の相互変換プログラム
これをCでやると…
/* * ISBN 変換プログラム * */ #include <stdio.h> #include <stdlib.h> #include <string.h> /* '0' -> 0 */ int ctoi(const char c) { return (c - '0'); } /* modulus n */ int modulus(int n, int weight) { return n - (weight % n); } /* ISBN10チェックデジット */ int isbn10CheckDigit(const char *isbn10) { int i, weight = 0; // Weight10-2 の部分 for (i=0; i<9; i++) weight += ctoi(isbn10[i]) * (10 - i); // modulus 11 の部分 return modulus(11, weight); } /* ISBN13チェックデジット */ int isbn13CheckDigit(const char *isbn13) { int i, weight = 0; // Weight3 for(i=0; i<12; i++) weight += ctoi(isbn13[i]) * (1 + (i % 2) * 2); // modulus 10 return modulus(10, weight); } /* ISBN10 to ISBN13 */ char *zero2three(const char *isbn10, char *dest) { char isbn13[14] = "978"; // 変換処理 strcat(isbn13, isbn10); isbn13[12] = isbn13CheckDigit(isbn13) + '0'; // 返す strcpy(dest, isbn13); return dest; } /* ISBN13 to ISBN10 */ char *three2zero(const char *isbn13, char *dest) { int cd; char isbn10[11] = ""; // 変換処理 strcat(isbn10, isbn13 + 3); cd = isbn10CheckDigit(isbn10); cd = (cd == 10) ? 'X' : (cd + '0'); isbn10[9] = cd; // 返す strcpy(dest, isbn10); return dest; } /* ISBN10 -> ISBN13ラップ */ char *php_zero2three(const char *isbn10) { // 保存先 char *result; if ((result = (char *)malloc(sizeof(char) * 14)) == NULL) return NULL; // 実行 zero2three(isbn10, result); return result; } /* ISBN13 -> ISBN10ラップ */ char *php_three2zero(const char *isbn13) { // 保存先 char *result; if ((result = (char *)malloc(sizeof(char) * 11)) == NULL) return NULL; // 実行 three2zero(isbn13, result); return result; } /* 簡易テスト */ void tst(const char *code) { char *(*func[])() = { php_zero2three, php_three2zero, }; int index; char *p1, *p2; // ISBN10 -> 0, ISBN13 -> 1 index = (strlen(code) == 10) ? 0 : 1; // 変換 p1 = (*func[index])(code); p2 = (*func[++index % 2])(p1); // 結果 printf("%s --> ", code); printf("%s --> ", p1); printf("%s --> ", p2); printf("%s\n", (strcmp(code, p2) == 0) ? "OK!" : "..."); // 解放 free(p1); free(p2); } int main(int argc, char *argv[]) { tst("4840147272"); tst("9784840147279"); tst("4840148775"); tst("9784840148771"); return 0; }
といった感じに。「isbn.c」とでも付けておきましょうか。
ISBNの変換については、main関数内の4件しかテストしていないので、実際に使うならちゃんとテストしないとダメですが。ハイフンの処理とか、ちゃんと文字数見とくとかもしとかないとならないし。あぁ、あとPHPに組み込んだ際は「バイナリセーフではない関数」となります。当然ですが。
最近、Interfaceばっかり読んでるせいか、「うわぁ…4bitあれば表せる整数を返すのに、int(4byte:32bit)使ってるよー…uint8_tとか、unsigned charにしとこうかなー…」とかいろいろ思い浮かぶけど、そのままにしておいた。メモリ節約できてもCPU使うならアレだし。SWIGが処理してくれるか分からないし。…というか、そんな細かいところが問題になるんだったら、PHPとの連携以前に、PHPなんて使わないで…。
で、インターフェースファイルというのが…
%module isbn %newobject php_zero2three; %newobject php_three2zero; extern char *php_zero2three(const char *isbn10); extern char *php_three2zero(const char *isbn13);
「isbn.i」とでも名前付けましょうか。ここには、PHP側で使いたいものだけ載せれば大丈夫です。
ところで、「zero2three」という関数を「php_zero2three」という関数でラップしてますよね。
本当は、直接zero2threeを使いたかったんですが、「zero2threeでの"char *dest"は14byte分必要だけど、PHP側で分かってくれんじゃねwwwww」と安易に考えてたら、さすがにPHPでも無理な要求のようで、SegmentationFault出まくりだったり、先頭の文字だけintで返ってきたりと大変だったので、こんな形にしました。なお、数値とかを返す場合はかなり簡単になります。複雑なのは文字列とか配列のときだけ。
本当は、この方法だとメモリをどこかで解放してやる必要があり、それがSWIGに分かるのか…?と思ったんですが、SWIGによって作成された「isbn_wrap.c」というものの一部を見ると…
result = (char *)php_zero2three((char const *)arg1); { if(!result) { ZVAL_NULL(return_value); } else { ZVAL_STRING(return_value, (char *)result, 1); } } free(result); return;
だいたい1112行目~1120行目を抜き出しました。
ちゃんと「PHPに値を渡したあとfree」してます。超賢い。
ちなみにこれは、「%newobject」を消すと、freeしてくれません。「%newobject」付けましょう。
ついでに「zero2three」をまんまで使っていたときのインターフェースファイル。別に見なくてもいいんだけど、どこかで役立つかも知れません。「char *OUTPUT」には相当悩まされました…。
%module isbn %include <typemaps.i> %apply signed char *INPUT {char *input}; %apply signed char *OUTPUT {char *output}; extern char *zero2three(const char *in, char *output); extern char * three2zero(const char *in, char *output);
PHPから利用する
さて、こうして出来た「isbn.c」と「isbn.i」ですが、
$ swig -php isbn.i $ gcc -fpic -c isbn.c $ gcc `php-config --includes` -fpic -c isbn_wrap.c $ gcc -shared *.o -o isbn.so $ sudo cp isbn.so `php-config --extension-dir` (のあとに、php.iniに「extension=isbn.so」って打つ)
として利用できるようにします。
でも、こんなのは
#/bin/bash # # Swig で PHP を高速化 # # 引数 if [ $# -lt 2 ]; then echo "Use: $0 input.i input.c" exit 1 fi # やるぜー超やるぜー BASENAME=`basename $1 .i` swig -php $1 gcc -fpic -c $2 gcc `php-config --includes` -fpic -c ${BASENAME}_wrap.c gcc -shared *.o -o ${BASENAME}.so # 任せるやつ echo "これやる" echo "cp $BASENAME.so `php-config --extension-dir`" echo "echo 'extension=$BASENAME.so' >> /etc/php.ini"
みたいなシェルスクリプトにしてしまえばいいのです。
では、これをPHPから使ってみましょう。
<?php include("isbn.php"); var_dump(isbn::php_zero2three("4840147272")); var_dump(isbn::php_three2zero("9784840147279")); ?>
「isbn.php」はSWIGが勝手に作るファイルで、「isbn.i」と同じディレクトリにあるはずです。
で、実行結果は…
string(13) "9784840147279" string(10) "4840147272"
はい、ちゃんと実行されているようです。
PHPとの速度比較
気になる速度比較です。PHPでも変換スクリプトを書いてみましょう。
<?php /* 関数 */ // ISBN13チェックデジット function isbn13CheckDigit($isbn13) { $weight = 0; for($i=0; $i<12; $i++) { $weight += (int)$isbn13[$i] * (1 + ($i % 2) * 2); } return 10 - ($weight % 10); } // ISBN10チェックデジット function isbn10CheckDigit($isbn10) { $weight = 0; for ($i=0; $i<9; $i++) { $weight += (int)$isbn10[$i] * (10 - $i); } return 11 - ($weight % 11); } // ISBN10 -> ISBN13 function zero2three($isbn10) { $isbn13 = "978".$isbn10; $isbn13[12] = (string)isbn13CheckDigit($isbn13); return $isbn13; } // ISBN13 -> ISBN10 function three2zero($isbn13) { $isbn10 = substr($isbn13, 3); $cd = isbn10CheckDigit($isbn10); $isbn10[9] = ($cd === 10) ? 'X' : (string)$cd; return $isbn10; } echo zero2three("4840147272")."\n"; echo three2zero("9784840147279")."\n"; ?>
ここでようやく、C言語版を「php_zero2three」なんて名前にしたのは間違いだったことに気付く…。
ふることきっ!で現役稼働中のISBN10(ASIN)とISBN13変換関数があるのですが、実は他人様がお作りになったもので、「Cと速度比較しPHPがいかにクソか見る記事」ですので、こんなところで人のコードを使うわけにはいきません。なので、自作しました。と言っても、CのまんまPHPにしたような感じ。文字長さ見ていないところとかも。
測定のコードですが、includeの時間などを考慮したくないため、こんな感じに。それぞれ100万回ループしてます。
<?php include("isbn.php"); require_once 'Benchmark/Timer.php'; define("LCOUNT", 1000000); $timer = new Benchmark_Timer(); $timer->start(); /* PHP Extension として実装 */ $timer->setMarker("PHP Extension"); for ($i=0; $i<LCOUNT; $i++) { isbn::php_zero2three("4840147272"); isbn::php_three2zero("9784840147279"); } /* Pure PHP */ $timer->setMarker("Pure PHP"); for ($i=0; $i<LCOUNT; $i++) { zero2three("4840147272"); three2zero("9784840147279"); } $timer->stop(); $timer->display(); /* Pure PHP 用関数 */ // ISBN13チェックデジット function isbn13CheckDigit($isbn13) { $weight = 0; for($i=0; $i<12; $i++) { $weight += (int)$isbn13[$i] * (1 + ($i % 2) * 2); } return 10 - ($weight % 10); } // ISBN10チェックデジット function isbn10CheckDigit($isbn10) { $weight = 0; for ($i=0; $i<9; $i++) { $weight += (int)$isbn10[$i] * (10 - $i); } return 11 - ($weight % 11); } // ISBN10 -> ISBN13 function zero2three($isbn10) { $isbn13 = "978".$isbn10; $isbn13[12] = (string)isbn13CheckDigit($isbn13); return $isbn13; } // ISBN13 -> ISBN10 function three2zero($isbn13) { $isbn10 = substr($isbn13, 3); $cd = isbn10CheckDigit($isbn10); $isbn10[9] = ($cd === 10) ? 'X' : (string)$cd; return $isbn10; } ?>
「PEAR::Benchmark」というものがあったのでそれを利用しました。
結果はこちら(5回実行)
$ php phpisbn.php ------------------------------------------------------------ marker time index ex time perct ------------------------------------------------------------ Start 1356836671.75255900 - 0.00% ------------------------------------------------------------ PHP Extension 1356836671.75261400 5.5074691772461E-5 0.00% ------------------------------------------------------------ Pure PHP 1356836673.89267000 2.1400558948517 15.42% ------------------------------------------------------------ Stop 1356836685.62934500 11.736675024033 84.58% ------------------------------------------------------------ total - 13.876785993576100.00% ------------------------------------------------------------ $ php phpisbn.php ------------------------------------------------------------ marker time index ex time perct ------------------------------------------------------------ Start 1356836701.71043200 - 0.00% ------------------------------------------------------------ PHP Extension 1356836701.71056000 0.00012803077697754 0.00% ------------------------------------------------------------ Pure PHP 1356836703.94691500 2.2363548278809 16.39% ------------------------------------------------------------ Stop 1356836715.35513200 11.408217191696 83.61% ------------------------------------------------------------ total - 13.644700050354100.00% ------------------------------------------------------------ $ php phpisbn.php ------------------------------------------------------------ marker time index ex time perct ------------------------------------------------------------ Start 1356836736.05128700 - 0.00% ------------------------------------------------------------ PHP Extension 1356836736.05134200 5.5074691772461E-5 0.00% ------------------------------------------------------------ Pure PHP 1356836738.66007700 2.6087350845337 18.60% ------------------------------------------------------------ Stop 1356836750.07345900 11.413381814957 81.40% ------------------------------------------------------------ total - 14.022171974182100.00% ------------------------------------------------------------ $ php phpisbn.php ------------------------------------------------------------ marker time index ex time perct ------------------------------------------------------------ Start 1356836831.36718700 - 0.00% ------------------------------------------------------------ PHP Extension 1356836831.36724300 5.6028366088867E-5 0.00% ------------------------------------------------------------ Pure PHP 1356836833.50995000 2.1427068710327 15.72% ------------------------------------------------------------ Stop 1356836845.00072400 11.490774154663 84.28% ------------------------------------------------------------ total - 13.633537054062100.00% ------------------------------------------------------------ $ php phpisbn.php ------------------------------------------------------------ marker time index ex time perct ------------------------------------------------------------ Start 1356836944.93728300 - 0.00% ------------------------------------------------------------ PHP Extension 1356836944.93740700 0.00012397766113281 0.00% ------------------------------------------------------------ Pure PHP 1356836947.24466500 2.3072578907013 16.69% ------------------------------------------------------------ Stop 1356836958.76093900 11.516273975372 83.31% ------------------------------------------------------------ total - 13.823655843735100.00% ------------------------------------------------------------
マーカーの関係でちょっと変ですが、まぁ分かりますよね。
実行時間をグラフ化してみます。
ということで、だいたい500%~600%くらい高速化されました。よかったよかった。
gccで「-O3」した場合
$ gcc -fpic -c isbn.c -O3 $ gcc `php-config --includes` -fpic -c isbn_wrap.c -O3 $ gcc -shared *.o -o isbn.so -O3 # 効果あるんだろうか
こうしたときの結果を見てみましょう。
$ php phpisbn.php ------------------------------------------------------------ marker time index ex time perct ------------------------------------------------------------ Start 1356838175.77625100 - 0.00% ------------------------------------------------------------ PHP Extension 1356838175.77630600 5.4836273193359E-5 0.00% ------------------------------------------------------------ Pure PHP 1356838177.86444500 2.0881390571594 15.32% ------------------------------------------------------------ Stop 1356838189.40420100 11.539756059647 84.68% ------------------------------------------------------------ total - 13.627949953079100.00% ------------------------------------------------------------ $ php phpisbn.php ------------------------------------------------------------ marker time index ex time perct ------------------------------------------------------------ Start 1356838219.70795500 - 0.00% ------------------------------------------------------------ PHP Extension 1356838219.70801100 5.6028366088867E-5 0.00% ------------------------------------------------------------ Pure PHP 1356838222.37394200 2.6659309864044 18.31% ------------------------------------------------------------ Stop 1356838234.26567800 11.891736030579 81.69% ------------------------------------------------------------ total - 14.557723045349100.00% ------------------------------------------------------------ $ php phpisbn.php ------------------------------------------------------------ marker time index ex time perct ------------------------------------------------------------ Start 1356838238.84396500 - 0.00% ------------------------------------------------------------ PHP Extension 1356838238.84401900 5.3882598876953E-5 0.00% ------------------------------------------------------------ Pure PHP 1356838241.23579900 2.3917801380157 16.49% ------------------------------------------------------------ Stop 1356838253.34481800 12.109019041061 83.51% ------------------------------------------------------------ total - 14.500853061676100.00% ------------------------------------------------------------ $ php phpisbn.php ------------------------------------------------------------ marker time index ex time perct ------------------------------------------------------------ Start 1356838267.43721900 - 0.00% ------------------------------------------------------------ PHP Extension 1356838267.43728100 6.1988830566406E-5 0.00% ------------------------------------------------------------ Pure PHP 1356838269.56607300 2.1287920475006 16.20% ------------------------------------------------------------ Stop 1356838280.57900100 11.012928009033 83.80% ------------------------------------------------------------ total - 13.141782045364100.00% ------------------------------------------------------------ $ php phpisbn.php ------------------------------------------------------------ marker time index ex time perct ------------------------------------------------------------ Start 1356838283.91719400 - 0.00% ------------------------------------------------------------ PHP Extension 1356838283.91725000 5.6028366088867E-5 0.00% ------------------------------------------------------------ Pure PHP 1356838286.17126300 2.2540130615234 16.47% ------------------------------------------------------------ Stop 1356838297.60437800 11.433115005493 83.53% ------------------------------------------------------------ total - 13.687184095383100.00% ------------------------------------------------------------
…まぁ、「言われるとそんな気がする」程度ですかね。この効果と副作用とのバランスを考えて付けてください。
最後に
これで、Cの関数を少しの手間をかけるだけでPHPで実行出来るようになりました。そのうち使おうと思います。
まだよく調べていないので分からないんですが、Webからある特定のサイトの画像にOCRをしたいと思っていて、「nhocr」というものを使おうと思っていて。なんか、SWIG使えば簡単に出来そうですよねー。
そして、今回参考にしたところ。
- Thought ISBN10とISBN13 - @//メモ(ISBN)
- SWIG1.3 ドキュメント日本語版(少し古いが公式ドキュメント日本語訳)
- SWIG-2.0 Documentation(公式ドキュメントPHP部分)
- 西谷・平山研Hiki - SWIG(日本語でまとまってる)
- nesugi.net - swigの使い方のメモ書き(%newobject)
- php swigでフィボナッチしてみた: 川はよこぎるものだ(全面的にパク...参考にさせてもらった)
- SWIG を使って PHP 拡張機能を作成する(これも全面的に参考にさせてもらった。でも、freeしなくていいのかとか心配になる)