カテゴリー別アーカイブ: PHP

phpの配列はどのようにして初期化され実行されるのか

概要

phpなどのLLは、記述するだけでコンパイルなしに実行されますがその中身はどうなっているのでしょうか。
今回は配列を例にとって、実際にphp処理系をおってみます。
主に字句解析、構文解析の実装について順を追って解説していきたいと思います。

zend処理系

まずはphpの処理系について全体像を軽く解説します。
ドキュメントも結構まとまっておりこの辺りなど非常に参考になります。(4年前ですがこの勉強会行きたかったな・・・)

さて、話を戻しますがphpのコアな部分はzend処理系と呼ばれる実装によって成り立っています。
zend frameworkというフレームワークが存在しますが、そちらのフレームワークとしてのzendではなくてphpの初期の発展に寄与したグループのzendです。

zend処理系ではphpのコードから実際に処理を実行するまでに下記のフェーズを実行します。

  1. 字句解析
  2. コードを字句(トークン)に分解します。トークンとは文が構成される最小単位のことです

  3. 構文解析
  4. 分解されたトークンを実際にどういった命令であるかということを解釈します

  5. OPcodeに翻訳
  6. 解釈した公文をOPcodeという中間コードに翻訳します

  7. 実行
  8. 実際にOPcodeを実行します

通常のアプリケーション開発なんかで言うとOPcodeなどは聞いたことがある方も多いのではないでしょうか。
有名どころではOPcacheなどがありますね。

OPcacheはその名の通りOPcodeをキャッシングします。
phpのコードに対して字句解析・構文解析を実行することで中間コードを生成するのですが、同じコードを毎回解析するのはかなり無駄なコストですね。
この中間コードをキャッシングすることで実行性能を上昇させることを目的とした機能です。
余談ですがこちらはphp5.5以降では標準搭載されるようになっていますね。

実際にはその前のフェーズとして字句解析・構文解析が行われます。
これらはごく普通にphpアプリケーションを開発しているだけだとあまり関わることがありません。今回はこちらについて掘り下げていきたいと思います。

字句解析

まず字句解析について。先に簡単に説明したようにコードを字句(トークン)に分解する作業です。

イメージしやすいように下記のサンプルコードを実行してみます。

<?php
$tokens = token_get_all('<?php
$hoge = array();');
foreach ($tokens as $i => $token) {
	echo is_array($token) ? token_name($token[0]) : $token;
	echo PHP_EOL;
}

echo PHP_EOL;

$tokens = token_get_all('<?php
$fuga = [];');
foreach ($tokens as $i => $token) {
	echo is_array($token) ? token_name($token[0]) : $token;
	echo PHP_EOL;
}

token_get_allはphpコードを引数として受け取り、そのコードをトークンレベルまで分解します。
またtoken_nameは分解されたトークンにzend処理系で管理している enum yytokentype を取得します。詳しくは zend_language_parser.c などを参照しましょう。

さて、サンプルコードの実行結果は下記のようになります。

T_OPEN_TAG
T_VARIABLE
T_WHITESPACE
=
T_WHITESPACE
T_ARRAY
(
)
;


T_OPEN_TAG
T_VARIABLE
T_WHITESPACE
=
T_WHITESPACE
[
]
;

T_OPEN_TAGはphpの開始タグを表すenum定数になります。またT_WHITESPACEはホワイトスペースを表すenum定数になります。
このようにphpのセンテンスをzend処理系で用意されたトークンに細かく分解していきます。

さらにここでarrayはT_ARRAYにトークン解析されていることがわかります。またそのあとのカッコについてはそのままになっていることがわかりますね。
これはそれ自体で意味を持っているメタ文字になるからです。

各種zend処理系での予約後に関してはトークン解析によってenumに置き換えられ、それ以外はメタ文字としてそのまま認識されることになります。

構文解析

続いて解析できたトークンを意味のある文として解釈します。zend処理系ではこの構文解析の実装とbisonと呼ばれるgnuツールを利用しています。bisonはBNFを基本とした亜種で文法を表現します。

bisonについてはこちらでかなり詳しく説明しておられるので参考にしてみると良いでしょう。

bisonでは終端トークン(最小単位のトークン)を組み合わせて文として認識できる構成を定義していきます。
zend処理系における実装はzend_language_parser.yに記述されておりますので確認してみてください。

例えば下記のようなコードが合った時

<?php
$a = 'str';

先に述べたようにこれらはまずトークン解析され、続いてbisonによって記述された文に一致するパターンを検索します。
もし文が適合すれば処理しますし、仮に文が解釈できないようであればパースエラーとして実行されないで終了します。
bison(およびBNF記法)の強力なところはそれぞれの文章を再帰的に表現することや、文の定義に他の文を含むことができるので非常に柔軟に文を定義できるところにあります。

arrayの文を確認する

構文解析まで理解できたところで実際にphpではarrayというセンテンスをどのように解釈して処理系が動作しているのかというところを追いかけてみます。

例えば下記のようなphpコードを対象として取り上げてみてみましょう。

$a = array();

字句解析の項目で解説したようにarrayという文字列はT_ARRAYとしてトークナイズされます。bisonの定義ファイルからこれの定義を探してみます。

%token T_ARRAY           "array (T_ARRAY)"

すると上記のような項目が見つかりました。これはトークンT_ARRAYを定義していることを表します。
続いてT_ARRAYを文として定義している箇所(=構文解析を行っていると思われる箇所)を探してみましょう。

combined_scalar:
		T_ARRAY '(' array_pair_list ')' { $$ = $3; }
	|	'[' array_pair_list ']' { $$ = $2; }
;

見つかりましたcombined_scalarという文が定義されており、トークンとしてT_ARRAYが使用されていることから、これが配列の初期化としての文であることが理解できます。

bisonではパイプで区切ることで複数の文を定義することができます。
よって二つ目の文についてはPHP5.4以降で新たに定義されたブラケット(カギカッコ)による配列の定義が確認できますね。
カギカッコの中は構文により呼び出されるc言語による実装です。$$は戻り値を示しており、$xはx番目にマッチするトークンを表しています。

さてここで先に述べたようにbison(BNF)では文を再帰的に定義できますし、文の定義に他の文の定義を含むことができます。
ここではarray_pair_listという文が定義されていることが確認できるので更に文の定義を追いかけます。

array_pair_list:
		/* empty */ { zend_do_init_array(&$$, NULL, NULL, 0 TSRMLS_CC); }
	|	non_empty_array_pair_list possible_comma	{ $$ = $1; }
;

non_empty_array_pair_list:
		non_empty_array_pair_list ',' expr T_DOUBLE_ARROW expr	{ zend_do_add_array_element(&$$, &$5, &$3, 0 TSRMLS_CC); }
	|	non_empty_array_pair_list ',' expr			{ zend_do_add_array_element(&$$, &$3, NULL, 0 TSRMLS_CC); }
	|	expr T_DOUBLE_ARROW expr	{ zend_do_init_array(&$$, &$3, &$1, 0 TSRMLS_CC); }
	|	expr 				{ zend_do_init_array(&$$, &$1, NULL, 0 TSRMLS_CC); }
	|	non_empty_array_pair_list ',' expr T_DOUBLE_ARROW '&' w_variable { zend_do_add_array_element(&$$, &$6, &$3, 1 TSRMLS_CC); }
	|	non_empty_array_pair_list ',' '&' w_variable { zend_do_add_array_element(&$$, &$4, NULL, 1 TSRMLS_CC); }
	|	expr T_DOUBLE_ARROW '&' w_variable	{ zend_do_init_array(&$$, &$4, &$1, 1 TSRMLS_CC); }
	|	'&' w_variable 			{ zend_do_init_array(&$$, &$2, NULL, 1 TSRMLS_CC); }
;

possible_comma:
		/* empty */
	|	','
;

array_pair_listの文を確認するとempty(0個のトークンで構成される)またはnot_empty_array_pair_list possible_commaで構成されることがわかります。

possible_commaの文定義を確認するとコンマもしくはなにもなし、と定義されることがわかります。PHPの配列では末尾にコンマをつけてもつけなくても認識してくれますが、それはこの文定義によるものだということが理解できますね。

さらにnot_empty_array_pair_listを確認してみます。定義がそれぞれありますがphpの多様な配列定義を文として定義しています。
例えばphpのセンテンスとして下記のようなものがありますが、それぞれがこの構文として定義されています。

<?php
$array1 = array('key' => value);
$array2 = array(1, 2);
$array3 = array();

最終的にzend_do_init_arrayというcの関数が呼び出されていることがわかります。
今回は構文解析からarrayの処理系の解釈を追うことが目的ですので、zend_do_init_arrayについては次の機会に取り上げたいと思います。

まとめ

いかがでしたでしょうか。
意外とおってみるとそんなに難しくないことがわかります。これもbisonなど先人が作り出してきた便利なツールによるものですね。


phpのcount関数の実装を見る

ご無沙汰しております。
今日は掲題のようにcount関数の実装を見て行きたいと思います。

概要

phpの開発を行ったことがある方であれば、下記のようなコード見たことあると思います。

$something = array('a', 'b', 'c');

for($i=0; $i<count($something); $i++) {
 // do something
}

また下記のようなコードも目にすることがあると思います。

$something = array('a', 'b', 'c');
$count = count($something);

for($i=0; $i<$count; $i++) {
 // do something
}

これらのコードどちらが効率的なのでしょうか。
なんとなく後者のほうが効率が良いようなことは想像に難しくないと思います。
でも、はっきり前者は悪だと断言できる方は実は少ないのではないでしょうか。

後者にしておけば困ったことにはならないでしょう。ただし、コードが一行増える。微々たるものですが。
どちらかと言うと前者のほうがコードとしてはすっきりしていると思います。

それに何より中身がどうなっているのかわからないのが気分的にすっきりしない。
今日はphpのcount関数の実装を確認してみたいと思います。

調査

php公式からソースコードをダウンロードして調査します。
バージョンは現在の最新安定版である5.6.14としました。

zval構造体

まずphpのzend構造体から確認します。
zend.hをみてみましょう。

typedef struct _zval_struct zval;
struct _zval_struct {
    /* Variable information */
    zvalue_value value;       /* value */
    zend_uint refcount__gc;
    zend_uchar type;          /* active type */
    zend_uchar is_ref__gc;
};

typedef union _zvalue_value {
    long lval;        /* long value */
    double dval;      /* double value */
    struct {
        char *val;
        int len;
    } str;
    HashTable *ht;    /* hash table value */
    zend_object_value obj;
    zend_ast *ast;
} zvalue_value;

phpでは変数はzval構造体で表されます。
zval構造体は参照カウントや、変数実体を保持するzvalue_valueという共用体を更に内部で保持します。
union構造体とは宣言された値の内どれか一つを保持する構造体のことです。

phpではlongやdoubleやstringなどの値に応じてzvalue_value共用体が保持する実体を切り替えるようになっています。
配列変数の場合はHashTable構造体を保持することになります。

Hashtable構造体

さらにzend_hash.hファイルを確認してその構造を確認してみましょう。

struct _hashtable;
typedef struct _hashtable {
    uint nTableSize;
    uint nTableMask;
    uint nNumOfElements;
    ulong nNextFreeElement;
    Bucket *pInternalPointer;       /* Used for element traversal */
    Bucket *pListHead;
    Bucket *pListTail;
    Bucket **arBuckets;
    dtor_func_t pDestructor;
    zend_bool persistent;
    unsigned char nApplyCount;
    zend_bool bApplyProtection;
 #if ZEND_DEBUG
    int inconsistent;
 #endif
} HashTable;

hashtable構造体については詳細な説明は今回は割愛します。
いったんデータ構造を確認したら今度はcount関数の実装を見てみることにしましょう。

count関数

array.cファイルを覗いてみます。

PHP_FUNCTION(count)
{
    zval *array;
    long mode = COUNT_NORMAL;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z|l", &array, &mode) == FAILURE) {
        return;
    }

    switch (Z_TYPE_P(array)) {
        case IS_NULL:
            RETURN_LONG(0);
            break;
        case IS_ARRAY:
            RETURN_LONG (php_count_recursive (array, mode TSRMLS_CC));
            break;
        case IS_OBJECT: {
#ifdef HAVE_SPL
            zval *retval;
#endif
            /* first, we check if the handler is defined */
            if (Z_OBJ_HT_P(array)->count_elements) {
                RETVAL_LONG(1);
                if (SUCCESS == Z_OBJ_HT(*array)->count_elements(array, &Z_LVAL_P(return_value) TSRMLS_CC)) {
                    return;
                }
            }
#ifdef HAVE_SPL
            /* if not and the object implements Countable we call its count() method */
            if (Z_OBJ_HT_P(array)->get_class_entry && instanceof_function(Z_OBJCE_P(array), spl_ce_Countable TSRMLS_CC)) {
                zend_call_method_with_0_params(&array, NULL, NULL, "count", &retval);
                if (retval) {
                    convert_to_long_ex(&retval);
                    RETVAL_LONG(Z_LVAL_P(retval));
                    zval_ptr_dtor(&retval);
                }
                return;
            }
#endif
        }
        default:
            RETURN_LONG(1);
            break;
    }
}

ごちゃごちゃと記述してありますが、今回は単純な配列な値の場合を確認します。
すると対象は一行だけでphp_count_recursiveを呼び出していることがわかります。

さらにphp_count_recursiveをおってみましょう。

PHPAPI int php_count_recursive(zval *array, long mode TSRMLS_DC) /* {{{ */
{
    long cnt = 0;
    zval **element;

    if (Z_TYPE_P(array) == IS_ARRAY) {
        if (Z_ARRVAL_P(array)->nApplyCount > 1) {
            php_error_docref(NULL TSRMLS_CC, E_WARNING, "recursion detected");
            return 0;
        }

        cnt = zend_hash_num_elements(Z_ARRVAL_P(array));
        if (mode == COUNT_RECURSIVE) {
            HashPosition pos;

            for (zend_hash_internal_pointer_reset_ex(Z_ARRVAL_P(array), &pos);
                zend_hash_get_current_data_ex(Z_ARRVAL_P(array), (void **) &element, &pos) == SUCCESS;
                zend_hash_move_forward_ex(Z_ARRVAL_P(array), &pos)
            ) {
                Z_ARRVAL_P(array)->nApplyCount++;
                cnt += php_count_recursive(*element, COUNT_RECURSIVE TSRMLS_CC);
                Z_ARRVAL_P(array)->nApplyCount--;
            }
        }
    }

    return cnt;
}

ここでも型チェックや再帰的に関数を呼び出している部分がありますが、本質的な部分は一行だけです。
変数cntを代入しているzend_hash_num_elementsを見てみましょう。
実装はzend_hash.cにあります。

ZEND_API int zend_hash_num_elements(const HashTable *ht)
{
    IS_CONSISTENT(ht);

    return ht->nNumOfElements;
}

IS_CONSISTENTマクロはハッシュテーブルが破壊されていないかどうかを確認するものです。
さてここでようやく実装が見えましたね。
count関数の行っていることはhashtable構造体のnNumOfElementsを返却しているだけなのです。

結論

冒頭で出した2つのコードのパフォーマンスの違いはそこまで大きくないでしょう、ビッグオー的に計算量で表すとどちらもO(1)になるからです。

ただ実装で言うとメソッドの呼び出しが3つほど噛まれていますので、関数スタックにガツガツと何度も積み上げられることになります。
特にパフォーマンスに気を使う部分であれば、count関数の呼び出しは一度だけに限定するほうが良いでしょう。

個人的にはfor文の中に挟み込む方式は、あまり推奨しないかな。
コードは行儀、シンプルに誰でもわかるように書きべきでしょう。


str_replace, preg_replaceのパフォーマンス検証と呼び出しの最適化

概要

PHPでは文字列を置換するのにstr_replaceとpreg_replaceという関数を用いることができます、今回はそれぞれのパフォーマンスについて考察していきたいと思います。

予想としては当然preg_replaceのほうがコストが高く付くと思います、実際本当にそうなのか。またどの程度のパフォーマンスの開きが出るのかをいくつかのサンプルを用意して比較します。
簡単な表現から、多少複雑な表現まで検証してみます。
またそれぞれの関数の使用時の最適化についても調べたいと思います。

ちなみに当然ですがPHPの 公式マニュアル では特別な必要性がない限りstr_replaceを使用することを推奨しています。

preg_replaceとstr_replaceを理解する

preg_replaceとstr_replaceについてはご存じの方も多いかと思いますが、その名のごとくある文字列を対象として、特定の単語を別の単語で置き換えることのできる関数になります。
その違いも簡単で、str_replaceは特定の単語を検索置換するのに対して、preg_replaceは正規表現を用いて検索置換が行えます。

str_replaceについて簡単な例を示します。

$before = "this is test.";
$after = str_replace("this", "that", $before);
var_dump($after); // "that is test."

シンプルですね。文字列の置換ができます。

つづいてpreg_replaceについても簡単な例を示します。

$before = "this costs 1000yen. it costs 2000yen.";
$after = preg_replace("/(\d+)yen/", "¥$1", $before);
var_dump($after); // "this costs ¥1000. it costs ¥2000."

こちらも単純に正規表現を用いての置換ができることがわかりました。シンプルです。

性能を検証する

さて気になるパフォーマンスを見て行きましょう。
今回はテストケースを3つ用意しました。
1. 単純なパターンの単数の置換
2. 単純なパターンの複数の置換
3. 複雑なパターンの単数の置換

検証したPHPのバージョンは下記になります

$ php -v
PHP 5.4.30 (cli) (built: Jul 29 2014 23:43:29)
Copyright (c) 1997-2014 The PHP Group
Zend Engine v2.4.0, Copyright (c) 1998-2014 Zend Technologies

単純なパターンの単数の置換を実行した際のパフォーマンス

ここでは単純なパターンでの一つの文字列の置換を実行します。
用意する計測するコードは下記のようになります。

<?php
if (!isset($argv[1])) {
  echo 'usage -- php profile.php {try_count}' . PHP_EOL;
}
$count = intval($argv[1]);
echo "run test {$count} times." . PHP_EOL;

$subject = "this is test sentence one. this is test sentence two. this is test sentence three. this is test sentence four. this is test sentence five. this is test sentence six. this is test sentence seven. this is test sentence eight. this is test sentence nine. this is test sentence ten.";
echo "check performance of str_replace." . PHP_EOL;
$start = microtime(true);
for($i=0; $i<$count; $i++) {
  $subject = str_replace('this', 'that', $subject);
}
$end = microtime(true);
var_dump($end - $start);

echo "check performance of preg_replace" . PHP_EOL;
$start = microtime(true);
for($i=0; $i<$count; $i++) {
  $subject = preg_replace('/this/', 'that', $subject);
}
$end = microtime(true);
var_dump($end - $start);

計測します。

$ php profile_test_2.php 100000
run test 100000 times.
check performance of str_replace.
double(0.15461802482605)
check performance of preg_replace
double(0.35560202598572)

このような単純なパターンでも約二倍程度の性能差があることが見て取れました。
予想通りの結果となり、検索置換をアプリケーションで行うようであれば特別な理由なくpreg_replaceを使用するべきでないですね。

単純なパターンの複数の置換を実行した際のパフォーマンス

続いて複数文字列を置換する際のパフォーマンスについて検証します。
下記のようにサンプルプログラムを用意します。

<?php
if (!isset($argv[1])) {
  echo 'usage -- php profile.php {try_count}' . PHP_EOL;
}
$count = intval($argv[1]);
echo "run test {$count} times." . PHP_EOL;

$subject = "this is test sentence one. this is test sentence two. this is test sentence three. this is test sentence four. this is test sentence five. this is test sentence six. this is test sentence seven. this is test sentence eight. this is test sentence nine. this is test sentence ten.";
$source = array('one', 'two', 'three', 'four', 'five', 'six', 'seven', 'nine', 'ten');
$dest = array(1,2,3,4,5,6,7,8,9,10);
echo "check performance of str_replace." . PHP_EOL;
$start = microtime(true);
for($i=0; $i<$count; $i++) {
  $subject = str_replace($source, $dest, $subject);
}
$end = microtime(true);
var_dump($end - $start);


$source = array('/one/', '/two/', '/three/', '/four/', '/five/', '/six/', '/seven/', '/nine/', '/ten/');
$dest = array(1,2,3,4,5,6,7,8,9,10);
echo "check performance of preg_replace" . PHP_EOL;
$start = microtime(true);
for($i=0; $i<$count; $i++) {
  $subject = preg_replace($source, $dest, $subject);
}
$end = microtime(true);
var_dump($end - $start);

計測します。

$ php profile_test_1.php 100000
run test 100000 times.
check performance of str_replace.
double(0.53684210777283)
check performance of preg_replace
double(1.4594769477844)

ここでも2倍から3倍程度の性能差が確認できました。
置換対象が複数になることでその性能差が開いていることが確認できます。

ここでちょっと気になるのは、str_replace, preg_replaceのそれぞれの性能を先の検証パターンと比較した時に性能が線形に悪化しているわけではなさそうだということです。
これについては別件として最後に検証を行うこととします。

複雑なパターンの単数の置換を実行した際のパフォーマンス

最後に複雑な正規表現を用いた際のパフォーマンスの違いについて見てみます。
下記のプログラムのように検索する表現を無駄に複雑にしてみます。

<?php
if (!isset($argv[1])) {
  echo 'usage -- php profile.php {try_count}' . PHP_EOL;
}
$count = intval($argv[1]);
echo "run test {$count} times." . PHP_EOL;

$subject = "this is test sentence one. this is test sentence two. this is test sentence three. this is test sentence four. this is test sentence five. this is test sentence six. this is test sentence seven. this is test sentence eight. this is test sentence nine. this is test sentence ten.";
echo "check performance of str_replace." . PHP_EOL;
$start = microtime(true);
for($i=0; $i<$count; $i++) {
  $subject = str_replace('this', 'that', $subject);
}
$end = microtime(true);
var_dump($end - $start);

$subject = "this is test sentence one. this is test sentence two. this is test sentence three. this is test sentence four. this is test sentence five. this is test sentence six. this is test sentence seven. this is test sentence eight. this is test sentence nine. this is test sentence ten.";
echo "check performance of preg_replace" . PHP_EOL;
$start = microtime(true);
for($i=0; $i<$count; $i++) {
  $subject = preg_replace('/([a-z]+) (is) (test)/', '$1 $2 $3', $subject);
}
$end = microtime(true);
var_dump($end - $start);

計測します。

$ php profile_test_3.php 100000
run test 100000 times.
check performance of str_replace.
double(0.14943814277649)
check performance of preg_replace
double(2.3758080005646)

すさまじい差ですね。(こんなケースはないかと思いますが)
正規表現による検索が、検索パターンによってコストが増大していることがわかります。

(番外編)置換対象が複数ある場合の関数呼び出し時の最適化について

検証パターン2の考察時にも触れましたが、複数の検索置換を行うときにstr_replace, preg_replaceは同様のインタフェースを用意しており、検索パターンと置換パターンを配列で渡すことができます。
複数の置換を実行するときに、置換を一回一回実行する方法と、置換パターンを配列で渡し一度に実行する方法の実装の仕方によってどれだけ差が出るかということを検証します。

プログラムを用意します。
一回一回実行する方法では、置換を行い、置換後の文字列に対して再度置換を行い、、、を繰り返します。

<?php
if (!isset($argv[1])) {
  echo 'usage -- php profile.php {try_count}' . PHP_EOL;
}
$count = intval($argv[1]);
echo "run test {$count} times." . PHP_EOL;

$subject = "this is test sentence one. this is test sentence two. this is test sentence three. this is test sentence four. this is test sentence five. this is test sentence six. this is test sentence seven. this is test sentence eight. this is test sentence nine. this is test sentence ten.";
echo "check performance of multiple call." . PHP_EOL;
$start = microtime(true);
for($i=0; $i<$count; $i++) {
  $subject = str_replace('one', 1, $subject);
  $subject = str_replace('two', 2, $subject);
  $subject = str_replace('three', 3, $subject);
  $subject = str_replace('four', 4, $subject);
  $subject = str_replace('five', 5, $subject);
  $subject = str_replace('six', 6, $subject);
  $subject = str_replace('seven', 7, $subject);
  $subject = str_replace('eight', 8, $subject);
  $subject = str_replace('nine', 9, $subject);
  $subject = str_replace('ten', 10, $subject);
}
$end = microtime(true);
var_dump($end - $start);

$subject = "this is test sentence one. this is test sentence two. this is test sentence three. this is test sentence four. this is test sentence five. this is test sentence six. this is test sentence seven. this is test sentence eight. this is test sentence nine. this is test sentence ten.";
echo "check performance of single call." . PHP_EOL;
$start = microtime(true);
$source = array('one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten');
$dest = array(1,2,3,4,5,6,7,8,9,10);
for($i=0; $i<$count; $i++) {
  $subject = str_replace($source, $dest, $subject);
}
$end = microtime(true);
var_dump($end - $start);

計測してみます。

 $ php profile_test_4.php 100000
run test 100000 times.
check performance of multiple call.
double(1.2376821041107)
check performance of single call.
double(0.51983189582825)

!!!
をを。思った以上に差が出てきた
約倍程度のパフォーマンスの差がでています。

と余談なんですが、他のPCでも同じプログラムの実行を行ったところ、上記の結果ほどの差は出なかった。
その際のバージョンは5.4.30であった。

$ php profile_test_4.php 100000
run test 100000 times.
check performance of multiple call.
float(0.44886112213135)
check performance of single call.
float(0.36096215248108)

なんだかしこり残るのでphpのバージョンを5.6にあげて検証してみます。

$ php -v
PHP 5.6.6 (cli) (built: Feb 20 2015 22:35:31)
Copyright (c) 1997-2015 The PHP Group
Zend Engine v2.6.0, Copyright (c) 1998-2015 Zend Technologies
    with Zend OPcache v7.0.4-dev, Copyright (c) 1999-2015, by Zend Technologies
    with Xdebug v2.2.5, Copyright (c) 2002-2014, by Derick Rethans
...
$ php profile_test_4.php 100000
run test 100000 times.
check performance of multiple call.
double(1.22190117836)
check performance of single call.
double(0.46425199508667)

悪化した!?!?!?

ここから考察できるのはどうやらphp5.4.30以降str_replaceの性能は悪くなっているようである。

どちらにしても最適化された呼び出しとしては配列で一度に渡したほうが高速である。

以上、もろもろパフォーマンスについて検証しました、参考にしていただければと思います。


PHPでYAMLファイルを取り扱う

概要

PHPでYAMLファイルを取り扱う方法について2つの方法を取り上げて解説したいと思います。
PHPでは純正のyamlライブラリが標準では無いため、別途用意する必要があります。

  • yaml拡張モジュール
  • yaml拡張モジュールとはlibyamlを用いた拡張モジュールになります。
    cで記述されているため高速に動作することが見込まれます。

  • Spyc
  • また完全にPHPのみを用いて記述されたyamlライブラリも存在し、そのうちの一つにSpycというも有名なのがあるのでこれも取り扱います。公式ページ
    特徴としては拡張モジュールはインストール作業が必要なのに対してSpycに関してはPHPのみで記述されているライブラリなので、複雑なインストール作業は不要です。
    1ファイルで構成されているのでリンク先からファイルをダウンロードし、Spyc.phpをrequireするだけで使用できる状態になります。非常にお手軽です。

    準備する

    それぞれの実装が使用できるように準備しましょう。

  • yaml拡張モジュール
  • 拡張モジュールはlibyamlおよびlibyaml-develを事前にインストールした状態でpeclコマンドを用いてインストールすることができます。
    rootになるまたはsudoコマンド経由で下記のようなコマンドを実行すれば完了です。

    [root@localhost ~]# yum install libyaml
    ...
    [root@localhost ~]# yum install libyaml-devel
    ...
    [root@localhost ~]# pecl install YAML
    ...
    
  • Spyc
  • 対してSpycは先にも述べましたがソースコードをダウンロードしてきて使用したい箇所でrequireするだけです。
    最近ではgithubでソースコードを管理されているようなので下記のようにcloneしてくれば十分だと思います。

    git clone https://github.com/mustangostang/spyc/
    

    検証してみる

    先にSpycで動かしてみましょう
    使用箇所でrequireします。するとグローバルでspyc_load_fileという関数が使用可能になります。
    これは引数としてファイルパスを与えることで、ファイルを読み込んでyaml形式でパースを行います。
    下記のような感じでOKです。

    <?php
    require_once (dirname(__FILE__) . '/Spyc.php');
    
    $obj = spyc_load_file('hoge.yaml');
    var_dump($obj);
    

    hoge.yamlは下記のように記述しておきましょう。

    - foo
    - bar
    

    すると出力結果は下記のようになります。

    array(2) {
      [0]=>
      string(3) "foo"
      [1]=>
      string(3) "bar"
    }
    

    動きましたね〜。

    対してyaml拡張モジュールではphp.iniにyaml.soライブラリをロードするように追記します。
    するとyaml_parse_fileという関数がグローバルで使用可能になります。
    こちらも同様に引数としてファイルパスを与えることで、ファイルを読み込んでパースを行います。

    $obj = yaml_parse_file('hoge.yaml');
    var_dump($obj);
    

    こちらも同様に、動きましたね。

    ただこの2つにはかなり挙動の違いがあるような箇所もあります。
    例えば入力ファイルの中身を下記のように編集したとしよう

    foo : bar
    bar
    

    この内容をパースすると

    Spycでは下記のようにパースされる。

    array(2) {
      'foo' =>
      string(3) "bar"
      [0] =>
      string(3) "bar"
    }
    

    たいしてyaml拡張モジュールではワーニングが表示され返り値としてはfalseがかえってきます。
    パースに失敗しているのです。

    PHP Warning:  yaml_parse_file(): scanning error encountered during parsing: could not find expected ':' (line 3, column 1), context while scanning a simple key (line 2, column 1) in /tmp/hoge.php on line 2
    bool(false)
    

    はたしてこれらは一体どちらが正しいのか。
    異なるフォーマットが与えられた時にちゃんとしてくれないのは困る。

    これに関していえるのはbarの扱いをどのように解釈しているのか、である。

    yaml1.1の仕様書を参考にしてみる。
    パラパラっと見た感じ、スカラ値のみで構成されるような場合は定義されていない。
    したがって結果として拡張モジュールでパースエラーが発生しているほうが安全であると言える。
    Spycではこれを意図的しているのかしていないのかは分からないが、純粋に配列の頭に付け加える(index:0)ことでパースを試みる。

    また別な例として下記の例もある。
    yamlではいくつかのメタ文字を定義していて
    例えば~はNULLを意味するしyはtrueを、nはfalseを意味する。などがあります。
    これらは型情報も一緒に解釈されるべきであります。

    実際に下記のようなyamlデータを用意して食わせてみましょう。

    null: ~
    true: true
    false: n
    string: '12345'
    

    Spycでの出力結果は下記のようになります。

    array(4) {
      'null' =>
      NULL
      'true' =>
      bool(true)
      'false' =>
      bool(false)
      'string' =>
      string(5) "12345"
    }
    

    yaml拡張モジュールでは下記のようになります。

    array(3) {
      [""]=>
      bool(false)
      [1]=>
      bool(true)
      ["string"]=>
      string(5) "12345"
    }
    

    驚いたことにyaml拡張モジュールでの出力結果は、はっきり言って構成を壊してしまっているといってもいいです。
    yaml界では一般的なのだろうか。

    対してSpycではメタ文字が使用されたとしてもキーとして使用されているうちは一意にstringとして解釈されるようです。
    拡張モジュールの方ではキーで使用される際もメタ文字として解釈され、結果わけのわからないことになっちゃってます。

    実際に用途として拡張モジュールのように解釈されてしまうと困ることのほうが困るような気がします。
    この辺りは逆にSpycのほうが直感に沿った解釈をしてくれているのかなと思います。

    好みのチョイスであるところもあるかもしれませんが、今回私はSpycの方をチョイスして実装の方を進めることにしました。
    今のところそこまで悪いところはなさそうです。
    (それに余談ですがインフラ都合もあって簡単にミドルモジュールをいじれるような状況でもないので)

    とりあえず、一点だけSpycで直して欲しいところがあったりして、spyc_load_fileファイルは読み込むyamlファイルパスを指定することでファイルを読み込めるのですが、引数に存在しないファイルを指定した際にもなんと普通に動いちゃいます

    たとえば下記のようなプログラムを実行すると

    <?php
    require_once (dirname(__FILE__) . '/Spyc.php');
    // aaaaは存在しないファイル
    $hoge = spyc_load_file('aaaa');
    var_dump($hoge);
    

    下記のように出力されます。

    array(1) {
      [0]=>
      string(4) "aaaa"
    }
    

    これまずくない??
    個人的には返り値がfalseになるとか例外投げるとかいろいろ設計はあると思うんですが。
    明らかに設計ミス?なにか意図があるのかと理解に苦しみます。

    と、、まあ今のところそんなこともあるのですが、また追加で問題などあれば追ってレポートしたいと思います。


    xhprofを利用する

    アプリケーションのパフォーマンスを向上させるためにはプロファイラと呼ばれるツールを利用するのが早いです。
    phpではxhprofというプロファイラがメジャーであり今回はその導入手順や使用感について紹介します。

    インストール

    PECLから入れます。そんなに難しくありません。

  • ソースをダウンロード
  • wget http://pecl.php.net/get/xhprof-0.9.2.tgz
    
  • make
  • tar zxvf xhprof-0.9.2.tar
    cd xhprof-0.9.2/extension/
    phpize
    make
    make install
    
  • php.iniを編集
  • [xhprof]
    extension=xhprof.so
    xhprof.output_dir=/var/log/xhprof
    
  • ログディレクトリを作成
  • mkdir /var/log/xhprof
    chmod -R 777 /var/log/xhprof
    
  • apache再起動
  • service httpd restart
    
  • xhprofのテンプレディレクトリをドキュメントルート配下に移動
  • cp -r /tmp/xhprof-0.9.2/xhprof_html {document_root}/xhprof_html
    cp -r /tmp/xhprof-0.9.2/xhprof_lib {document_root}/xhprof_lib
    
  • プロファイリングしたい箇所にコードを埋め込みます
  • {document_root}は適宜自分の環境に置き換えてください。

    xhprof_enable(); // プロファイリング開始
    
    //
    // ** プロファイリングしたい処理をここに記述 **
    //
    
    $xhprof_data = xhprof_disable();    //stop profiler
     
    //  プロファイリングページへのリンクを追加
    include_once "{document_root}/xhprof_lib.php";
    include_once "{document_root}/xhprof_runs.php";
    $xhprof_runs = new XHProfRuns_Default();
    $run_id = $xhprof_runs->save_run($xhprof_data, $XHPROF_SOURCE_NAME);
    echo "<a href=\"http://{document_root}/xhprof_html/index.php?run=$run_id&source=$XHPROF_SOURCE_NAME\">xhprof Result</a>\n";
    

    結果

    実行すると結果ページヘのリンクが表示されます。クリックすることで結果の詳細を確認することができます。

    おぉー参照できましたね。なかなかにいいかんじです。

  • 各項目
  • 項目 意味
    Function Name 関数名
    Calls プロファイリングした期間でコールされた回数
    Calls% コール回数のパーセント表示
    Incl. Wall Time 処理にかかった時間のうち、その関数が消費した時間。なお処理をネストした場合全経過時間を含むことになるので、あまり参考することはないと思われる
    IWall% Incl. Wall Timeのパーセント表示
    Excl. Wall Time 実際にその関数のみが消費した時間。多くの場合はこの項目なんかを参照しながらプロファイリングすることになると思われる
    IWall% Execl. Wall Timeのパーセント表示

    なるほどかなり正確な情報が把握できて、最終的にボトルネックになっている箇所が確認できます。
    実際に動かしてみると、当然ですが大きく時間を消費していたのがネットワーク処理、ついでファイル処理などかなり実用的なレベルで参考することができそうです。

    余談

    ちなみに結構長い間フリーランスしてる同僚と、プロファイリングからの性能検証について雑談してて。
    現実的な現場感としては、こういったプロファイリングを常に実行してボトルネックをチェックすることが多いのかな。ということについて話してると、現実的には実際に何か問題が発覚してから調査する。ってのが多いようです。

    あとは細かな性能検証も大事なんですが、昔と比べてマシンのスペックが格段に向上しているのでアプリのチューニングを突き詰めていくよりかは、マシンを追加してインフラ面からパワーを上げて対応する事が多いです。
    (もちろん普通にボトルネックになっているような場合は除く。アプリのチューニングが不要と言っているわけではないです。)

    実際その通りで、僕もどちらかと言うと小難しいコンパクトなコードを書くよりかは、コストが多少高くつくとしても可読性の高いコードを書くように心がけています。
    (よくある普通のレベルの企業では)あまり出番の多いわけではさそうですが、覚えておくと絶対役に立ちますね。
    個人的には常にこのへんの意識は頭のなかにあるエンジニアでいたいです。


    PHPのメモリ節約と参照渡しについて

    概要

    今回は以前から調べようと思って調べきれてなかった、参照渡しとを行うことでメモリの節約をできるのかということについてまとめたいと思います。

    トピックとしては下記のような。

    • コピーオンライト
    • 参照カウンタ、参照フラグ

    トピックおさらい

    PHPでは代入を行う際に基本的に何もしなければ値をコピーして渡します。
    参照を渡したい場合は&をつけることによって実現します。

    $a = 'str';
    $b = $a;
    $c = &$a;
    

    例えば上記のようなプログラムを作成した場合。
    $bには$aのコピーが作成され、$cには$aの参照が渡されます。

    ここで、「$bを使用することで、$aのコピーが作成されてしまうということはメモリが無駄になる。$cのように参照で保持しておいたほうがメモリの節約につながるのではないか」と思う方もいらっしゃるかもしれません。

    今日はこの辺について考察していきます。

    さて、私の考察の結論から先に書いてしまいますと「適切に参照渡しを使用することで、メモリの節約につながる」と思います。
    また合わせて「特に必要がなければ参照渡しを行う必要はない。これによってメモリが無駄になることもない」とも思っています。

    具体的に参照渡しするべき場所として考慮すべき時は

    • 巨大な構造を保持しているためメモリの消費を抑えたい
    • 参照元の変数(シンボル)と、参照先の変数が変更を共有できる(当たり前ですが)

    逆に参照渡ししなくてもメモリの消費につながらない時とは

    • 参照先の変数で値の変更を行わない※超重要

    であり、まさにここが今日のメイントピックでもあります。

    調査

    具体的に内部実装としてどのように先ほどの参照関係を保持しているかというと
    PHP内部で参照カウントとよばれる、値(変数コンテナ)を参照しているシンボルやリンクの数(refcount)および、参照フラグという変数コンテナを参照型で保持しているシンボルが存在するかどうかという情報(is_ref)を保持しています。

    例えば下記のコードに関して参照カウントと、参照フラグを見てみましょう。

    $a = 'str';
    $b = $a;
    $c = $b;
    

    このとき各シンボルの情報は下記のようになります。

    $a => (refcount => 3, is_ref => 0)
    $b => (refcount => 3, is_ref => 0)
    $c => (refcount => 3, is_ref => 0)
    

    すべての変数が同じ変数コンテナ(’str’)を参照している形になるので、納得ですね。

    次に冒頭に設置したコードだとどうなるかを確認してみましょう。

    $a = 'str';
    $b = $a;
    $c = &$b;
    

    非常におどろくべきところなのですが、参照カウントが2となっている変数コンテナと、参照カウントが1となっている変数コンテナにわかれました。

    $a => (refcount => 2, is_ref => 1)
    $b => (refcount => 1, is_ref => 0)
    $c => (refcount => 2, is_ref => 1)
    

    これはどういうことなのかというと、参照渡しによりシンボルに代入を行うと、そのタイミングで別個の変数シンボルが即座に作成されます。
    つまり変数コンテナのユニーク性にis_refフラグが関与しているということになります。

    上記の情報を元にもう一度、参照渡ししなくてもメモリの消費につながらない時について考えてみましょう。
    つまるところ、変数コンテナが作成される条件に注意していけば大丈夫です。

    悪いコードとして下記の例を上げましょう(ちょっと無理矢理かな・・・)

    // アプリケーションの設定項目なんかを参照で取得
    $config = &$app->_config;
    ...
    // 一旦退避とかしてみる
    $tmp = $config;
    

    上記の例では$tmpに$configを代入した段階で参照フラグの異なる変数コンテナを生成しています。
    このタイミングで一気にドカンと$app_configと参照フラグのみ異なり、内容の全く同じ変数コンテナが生成されメモリを逼迫します。

    結論

    じゃあどうすればいいのか・・・ということなんですが。

    結論、普通に使用していれば大丈夫です

    さきほどの例で言うと、下記のように通常参照にすれば良いと思います。

    // アプリケーションの設定項目なんかを参照で取得
    $config = $app->_config;
    ...
    // 一旦退避とかしてみる
    $tmp = $config;
    $tmp['url'] = $new_url;
    

    たとえば$app->_configが配列であった場合、配列のデータの持ち方として変数コンテナをネストするような形でちょうど、親子のような関係がうまれます。
    このときに上記のように一個の変数をいじるとしても、その末端の変数コンテナのみがコピー・変更されるだけで全体の他の項目は全く影響がないので、そこまでメモリなどについて心配する必要はありません。

    問題が起こるのは参照渡しを保持していた時のみということです。

    また冒頭のキーワードで示したようにコピーオンライトという方針に則ってメモリの最適化を図っています。
    これは変数を単純にコピーした時点においては、元々あった変数の変数コンテナをずっと参照し続けます。
    いざ、変更が入った段階で新しい変数コンテナを作成し、参照先をそちらに切り替えるのです。賢いですね。

    ちなみにこのコピーオンライトとはプログラミングの実装以外でも、OSのメモリ管理やファイル管理にも用いられているような技術ですね。

    本日はそろそろこのへんで。
    参照になれば幸いです。

    参考にしたページ
    PHP本家


    単調配列array_diffの最適化について

    簡単なarray_diffの検証用プログラム。

    皆さんご存知の通りphpにはarray_diffという引数指定した配列の差分だけを抽出する関数が備え付けであります。
    このarray_diffは配列が保持する値を比較します。

    機能的にはこれで十分要件を満たしてくれている場合が多いと思います。

    なんですが単調配列(連想配列ではない、オートインクリメントなインデックスによって保存される配列という意味で)においては
    これをarray_diff_keyとarray_flipを用いた処理に置き換えることができ、その処理速度について検討します。

    早速ですが簡単なテスト用のプログラムを用意

    ここでは100個の要素を持つ配列と、5この要素を持つ配列の差分を抽出します。

    if (!isset($argv[1])) {
      echo 'usage -- php profile.php {try_count}' . PHP_EOL;
    }
    $count = intval($argv[1]);
    
    $array1 = array(100,200,300,400,500,600,700,800,900,1000,1100,1200,1300,1400,1500,1600,1700,1800,1900,2000,2100,2200,2300,2400,2500,2600,2700,2800,2900,3000,3100,3200,3300,3400,3500,3600,3700,3800,3900,4000,4100,4200,4300,4400,4500,4600,4700,4800,4900,5000,5100,5200,5300,5400,5500,5600,5700,5800,5900,6000,6100,6200,6300,6400,6500,6600,6700,6800,6900,7000,7100,7200,7300,7400,7500,7600,7700,7800,7900,8000,8100,8200,8300,8400,8500,8600,8700,8800,8900,9000,9100,9200,9300,9400,9500,9600,9700,9800,9900,10000);
    $array2 = array(100,200,300,400,500);
    
    $start = microtime(true);
    for($i=0; $i<$count; $i++) {
      $array = array_diff($array1, $array2);
    }
    $end = microtime(true);
    var_dump($end - $start);
    
    $start = microtime(true);
    for($i=0; $i<$count; $i++) {
      $array = array_diff_key(array_flip($array1), array_flip($array2));
    }
    $end = microtime(true);
    var_dump($end - $start);
    

    で結果。10000回試行してみます。

    php profile.php 10000
    float(1.888808965683)
    float(0.19536399841309)
    

    すごい差ですね。
    array_flipのコストなんか関係なしにarray_diff_keyのほうが高速に参照できていることがわかります。

    ここまで来ると実装内部まで気になりますね。また暇を見つけて掲載できればなと思います。

    予想としては、ソートに最適化されたキー構造があるためやはりキーを下に検索すると早い。
    値の比較では、全キーに関して値を走査する必要が有るため、単純に総当り、ということになるんでしょう。
    件数が多ければ多いほど爆発的な違いなってくることが予想できます。

    単調な配列のdiffに関してはarray_diff_keyを利用すると良さそうですね。