PHPだけど速ささえあれば関係ないよねっ

closeこれは 1 年 7 ヶ月 6 日 前に投稿されたものです。最新のものではありませんので、間違っているかも知れません。

そんなこと無いわけで。

さて、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%
------------------------------------------------------------

マーカーの関係でちょっと変ですが、まぁ分かりますよね。

実行時間をグラフ化してみます。
php_swig
ということで、だいたい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使えば簡単に出来そうですよねー。

そして、今回参考にしたところ。

Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>