そんなこと無いわけで。
さて、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しなくていいのかとか心配になる)