Zephir使えばPHP Extensionが誰でも超絶簡単に作れる

More than 3 years have passed since last update.


はじめに

※ タイトルは、正確には、「Zephir使えばPHP Extensionが(PHPのコードさえかければ)誰でも超絶簡単に作れる」ですが、PHP Extensionを書きたい人はPHPのコードを書ける人だと思われますので、省略してます。

メモリ操作を気にせずにPHP Extensionを作れる言語”Zephir”

こちらの記事を読んで面白そうだなと気になってストックだけして積んでた状態でした。

しかし、とある案件でファイルのフルスキャンをしたくなったのでPHP Extension化を試みました。

情報は公式の

Zephir Language

にもありますが、日本語情報が少ないのでこちらに簡単にまとめます。


結論から箇条書きすると


  • 面白い。

  • 初めて触ったその日にメモリ管理に気を使う必要なく、PHP Extensionの開発が出来ました。

  • 今回のケースでは、PHPでベタ書きした場合と比較して4倍のスループットが出ました。

  • 多少PHPとは言語構造が異なりますが、Try and Errorで、(もしくは、英文ドキュメントをきちんと読めば)簡単に開発が出来そうです。

  • ビルド可能な環境さえあれば、運用サーバにはZephirの開発環境を構築する必要はありません。


やってみよう


インストール、サンプル作成

上記

メモリ操作を気にせずにPHPエクステンションを作れる言語”Zephir”

もしくは、英語が読める人は

Zephir Installation

を参考にインストールしてください。

私はVMWare上のCentOSで試したのですが、環境がほぼ同じだったので本当にサクッとインストール完了しました。

ただし、私の環境でzephirコマンドを利用しようとした時には以下の問題が発生しましたので参考まで。


bash: /usr/bin/zephir: No such file or directory


【対応】 ln -s /usr/local/src/zephir/bin/zephir /usr/bin/zephir


/usr/bin/zephir: line 11: cd: /usr/bin//usr/local/src/zephir/bin/..: No such file or directory

Environment variable ZEPHIRDIR is not set


【対応】 export ZEPHIRDIR=/usr/local/src/zephir/


作りたかったコードの外部仕様

指定されたCSVファイルから、指定されたフィールドがある値に完全一致する行を全て抽出するプログラム


まず、PHPで実装してみる

勿論、これをPHPスクリプトから直接includeしてもいいのですが、、、。


csv.php

<?php

namespace PHPSample;

class Csv
{
public static function filterOneFile($filename, $column_index, $value)
{
$rtn = array();
if($fp = @fopen($filename, "r")){
do{
$buffer = fgets($fp);
$values = explode(",", trim($buffer));
if(isset($values[$column_index]) && $values[$column_index] == "\"".$value."\""){
$rtn[] = trim($buffer);
}
}while($buffer !== false);
fclose($fp);
}else{
return false;
}
return $rtn;
}
}



まず適当にそれっぽく変換してみる


  • 開始の <?php は不要

  • ファイル名を *.zep にする

  • Namespaceをプロジェクトに合わせる。(今回はSample)

  • 各変数名先頭の $ を削除


  • @function()は使えないようなので@削除

  • 利用している変数を全て定義する(とりあえず全てvarで大丈夫)

  • 単純代入が出来ないようなのでletを使った代入にする


csv.zep(ビルドできません。)

namespace Sample;

class Csv
{
public static function filterOneFile(filename, column_index, value)
{
var fp;
var buffer;
array values; // var values; でも動作します。
array rtn;

let rtn = array();
if(let fp = fopen(filename, "r")){
do{
let buffer = fgets(fp);
let values = explode(",", trim(buffer));
if(isset(values[column_index]) && values[column_index] == "\"".value."\""){
let rtn[] = trim(buffer);
}
}while(buffer !== false);
fclose(fp);
}else{
return false;
}
return rtn;
}
}



ビルドしてみる

zephir build


Zephir\ParseException: Syntax error in /home/nakagawa/workspace/zephir/sample/sample/csv.zep on line 12

            let rtn = array();

----------------------^

→ どうやらarray()を使えないようなので、


sample

        let rtn = [];


に変更して再ビルドします。


修正後再ビルド

zephir build


Zephir\ParseException: Syntax error in /home/nakagawa/workspace/zephir/sample/sample/csv.zep on line 13

            if(let fp = fopen(filename, "r")){

--------------^

→ 代入して同時に評価が出来ないようなので、この部分を


sample

        let fp = fopen(filename, "r");

if(fp){

に書き換えます。


もう一度ビルド

zephir build


Preparing for PHP compilation...

Preparing configuration file...

Compiling...

Installing...

Extension installed!

Don't forget to restart your web server


正常に処理が完了した様子です。

この後、生成された、sample.soファイルをPHPのExtensionディレクトリに放り込んでから


echo "extension=sample.so" > /etc/php.d/sample.ini


でさくっと使えるようになるのは上記のページの紹介のとおりです。

Apacheモジュール版のPHPで起動させる場合にはApacheの再起動が必要ですが、コマンドライン版ではそのまま利用可能です。


ビルドできたソース


csv.zep

namespace Sample;

class Csv
{
public static function filterOneFile(filename, column_index, value)
{
var fp;
var buffer;
array values; // var values; でも動作します。
array rtn;

let rtn = [];
let fp = fopen(filename, "r");
if(fp){
do{
let buffer = fgets(fp);
let values = explode(",", trim(buffer));
if(isset(values[column_index]) && values[column_index] == "\"".value."\""){
let rtn[] = trim(buffer);
}
}while(buffer !== false);
fclose(fp);
}else{
return false;
}
return rtn;
}
}



他のCentOSサーバにExtensionを移植してみた

作成されたsample.soファイルをPHPのバージョンが同じ他のサーバにSCPして、PHPのExtensionディレクトリに放り込んでからの


echo "extension=sample.so" > /etc/php.d/sample.ini


であっさりサンプルスクリプトが動作。

特に問題なく利用できるようです。


ベンチマークを取りました。

Webページとして公開中のページに対する処理速度を調べるため、サンプルスクリプトを組んでApacheBenchを取ってみました。

サンプルスクリプトは、実践的なコードの一部にこの処理を追加という形で記載しておりますので、実際の運用に近いデータが取れる筈です。


利用部分コード

   $match_lines = Sample\Csv::filterOneFile($filename, $column_index, $column_value);


サンプルデータ

1ファイルあたり平均5500行のファイルを1,000ファイル設置、1リクエストあたり一つのファイルを選んでフルスキャンを行ない、マッチしたデータを探し出します。


ベンチマーク実行


まず、ダミースクリプトに対して実行

該当部分のソースで、予め定義済みのarray()を返答するため検索コストが一切かからない状態です。


ab -n 100 -c 10 "http://localhost/getMatchedLineByFile?engine=dummy"


Requests per second:    396.64 [#/sec] (mean)

Time per request: 25.211 [ms] (mean)
Time per request: 2.521 [ms] (mean, across all concurrent requests)
Transfer rate: 2042.49 [Kbytes/sec] received

→ このシステムにおける理論上の最速値。


PHPで実装したファイルマッチエンジンに対して実行

該当部分を最初に記載したPHPスクリプトを利用した場合。


ab -n 100 -c 10 "http://localhost/getMatchedLineByFile?engine=php"


Requests per second:    32.49 [#/sec] (mean)

Time per request: 307.764 [ms] (mean)
Time per request: 30.776 [ms] (mean, across all concurrent requests)
Transfer rate: 24.57 [Kbytes/sec] received

→ ファイル操作が入るため、がっつりスループットが落ち込んでます。


PHP Extensionを利用した場合

該当部分を先ほど作成したPHP Extensionに差し替えた場合


ab -n 100 -c 10 "http://localhost/getMatchedLineByFile?engine=extension"


Requests per second:    120.73 [#/sec] (mean)

Time per request: 82.828 [ms] (mean)
Time per request: 8.283 [ms] (mean, across all concurrent requests)
Transfer rate: 107.18 [Kbytes/sec] received

→ CPUを使ってファイルのフルスキャンをしているという事実は変わらないので、ダミーで返答しているケースと比べるとスループットは落ち込んでいますが、PHPでそのまま書いたケースと比較するとおよそ4倍速いです。


その他ちょこっとハマったポイントを箇条書きで

こちら、最初に公式ドキュメント全体に目を通しておけば多分回避出来たのですが、、、。


  • zephir build をzephir initで作られたルートディレクトリ以外で行おうとすると、

    Zephir\Exception: Extension namespace cannot be loaded

    となる。

    → 同じディレクトリのconfig.jsonを見ているようです。


  • for(let i=0; i<100; let i++){} と書いても動作しません。書き換えが必要でした。

    → for i in range(0, 100){}への書き換えが必要。


  • このサンプルでは出てきませんが、foreach(some_arr as value){} も利用できません。

    → for value in some_arr {}への書き換え。


  • 同じくlet rtn = array_merge(rtn, tmp_result)とした部分がビルド出来ませんでした。


for tmp in tmp_result{

let rtn[] = tmp;
}

に書き換えました。


最後に所感

既存のコードの重い部分をちょこっと書き換えるという用途で、(多少のメンテナンス性と引き換えに)結構使えそうです。