徹底検証!PHP最適化Tips

第1回世間で噂されるPHP最適化tipsについて検証する

はじめに

PHPに関する話題の中では、PHPコードをどのように書けば最適化されるのかに関するtipsがブログなどでもたびたび話題に出てきています。しかし、このようなtipsが本当に有効なのか、どうして有効なのか解説している記事は少なく、その信憑性は気になるとこです。

そこで、PHP最適化tipsについて紹介している記事、

をもとに、いくつか抜粋して検証していきます。

PHPソースコードの入手

やはり、内部の動きを知るにはソースコードを読むのが一番です。本稿でもソースコードをもとに解説を行います。

こちらから最新版のソースコードが入手できます。現在の最新バージョンは5.2.6になります。本稿ではPHP5.2.6をベースに解説していきます。

echoのほうがprintより速い

まずはじめに検証を行うのは、⁠echoのほうがprintより速い⁠というtipsです。echo、printはご存知のとおり文字列を出力します。

それでは実際に下記のコードを用いてベンチマークをとってみます。用意したコードは1000回ループを回して、echoとprintを実行した合計時間で評価を行うシンプルなコードです。それぞれのコードは5行目でechoを使っているか、printを使っているかの違いがあります。

benchmark_echo.php
<?php
ob_start();
$t = microtime(true);
while($i < 1000) {
   echo '';
   ++$i;
}
$tmp = microtime(true) - $t;
ob_end_clean();

var_dump($tmp);
?>
benchmark_print.php
<?php
ob_start();
$t = microtime(true);
while($i < 1000) {
   print '';
   ++$i;
}
$tmp = microtime(true) - $t;
ob_end_clean();

var_dump($tmp);
?>
実行結果
$ php benchmark_echo.php
float(0.000132083892822)
$ php benchmark_print.php
float(0.000159978866577)

わずかではありますが、echoのほうが速い結果になりました。

それでは、この違いはどこからくるものなのでしょうか。実際にコードを追ってみましょう。

printの実体はZend/zend_compile.c内のzend_do_print()にあります。zend_do_print()ではresultに返り値を必要としており、result変数に返り値が設定されています。

Zend/zend_compile.c
void zend_do_print(znode *result, znode *arg TSRMLS_DC)
{
   zend_op *opline = get_next_op(CG(active_op_array) TSRMLS_CC);

   opline->result.op_type = IS_TMP_VAR;
   opline->result.u.var = get_temporary_variable(CG(active_op_array));
   opline->opcode = ZEND_PRINT;
   opline->op1 = *arg; 
   SET_UNUSED(opline->op2);
   *result = opline->result;
}

echoの実体はZend/zend_compile.c内のzend_do_echo()にあります。zend_do_echo()ではzend_do_print()とは違い、返り値を必要としていないことがわかります。

Zend/zend_compile.c
void zend_do_echo(znode *arg TSRMLS_DC)
{
   zend_op *opline = get_next_op(CG(active_op_array) TSRMLS_CC);

   opline->opcode = ZEND_ECHO;
   opline->op1 = *arg;
   SET_UNUSED(opline->op2);
}

結果を返さない分、echoのほうが早くなります。

1. Speed. There is a difference between the two, but speed-wise it should be irrelevant which one you use. echo is marginally faster since it doesn't set a return value if you really want to get down to the nitty gritty.

http://www.faqts.com/knowledge_base/view.phtml/aid/1/fid/40

PHPの生みの親でもあるRasmusらが書いたこのエントリには、echoとprintには返り値があるかないかの違いはあるが、実行速度の違いは気にするほどのものではないと書かれおり、使い方によってecho, printを使いわけたほうがよさそうです。

@によるエラー制御は遅い

PHPではエラー制御演算子として@を使用することができます。PHPの式の前に付けることで、その式で発生したエラーメッセージを無視することができます。

新たにprintに@を付けたコードを用意し、先ほどと同様に実行してみます。

benchmark_use_atmark.php
<?php
ob_start();
$t = microtime(true);
while($i < 1000) {
   @print '';
   ++$i;
}
$tmp = microtime(true) - $t;
ob_end_clean();

var_dump($tmp);
?>
$ php benchmark_print.php
float(0.000159025192261)
$ php benchmark_use_atmark.php
float(0.000759124755859)

@を付けたほうが遅くなりました。遅い原因を探ってみましょう。

Zend/zend_language_parser.y
616 |@' { zend_do_begin_silence(&$1 TSRMLS_CC); } expr { zend_do_end_silence(&$1 TSRMLS_CC); $$ = $3; }

“@⁠が付いた構文に関する処理はZend/zend_language_parser.yで定義されており、zend_do_begin_silence()とzend_do_end_silence()構文が実行される前と後に実行されています。

次にzend_do_begin_silence()とzend_do_end_silence()について調べます。どちらの関数もZend/zend_compile.cで定義されてます。

Zend/zend_compile.c
void zend_do_begin_silence(znode *strudel_token TSRMLS_DC)
{
   zend_op *opline = get_next_op(CG(active_op_array) TSRMLS_CC);

   opline->opcode = ZEND_BEGIN_SILENCE;
   opline->result.op_type = IS_TMP_VAR;
   opline->result.u.var = get_temporary_variable(CG(active_op_array));
   SET_UNUSED(opline->op1);
   SET_UNUSED(opline->op2);
   *strudel_token = opline->result;
}
Zend/zend_compile.c
void zend_do_end_silence(znode *strudel_token TSRMLS_DC)
{
   zend_op *opline = get_next_op(CG(active_op_array) TSRMLS_CC);

   opline->opcode = ZEND_END_SILENCE;
   opline->op1 = *strudel_token;
   SET_UNUSED(opline->op2);
}

zend_do_begin_silenceとzend_do_end_silenceで設定されたopcodeとop_typeからhandlerが選ばれ、Zend/zend_vm_execute.h内のexecuteで選択されたhandlerが実行されます。

Zend/zend_vm_execute.h
30753 return zend_opcode_handlers[opcode * 25 + zend_vm_decode[op-.op_type] * 5 + zend_vm_decode[op-.op_type]];

その結果、zend_do_begin_silence()ではZEND_BEGIN_SILENCE_SPEC_HANDLERが、zend_do_end_silence()ではZEND_END_SILENCE_SPEC_TMP_HANDLERが呼ばれます。

次にZEND_BEGIN_SILENCE_SPEC_HANDLERとZEND_END_SILENCE_SPEC_TMP_HANDLERについて見ていきます。この関数もZend/zend_vm_execute.hに定義されています。

Zend/zend_vm_execute.h
static int ZEND_BEGIN_SILENCE_SPEC_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
   zend_op *opline = EX(opline);

   Z_LVAL(EX_T(opline->result.u.var).tmp_var) = EG(error_reporting);
   Z_TYPE(EX_T(opline->result.u.var).tmp_var) = IS_LONG;  /* shouldn't be necessary */
   if (EX(old_error_reporting) == NULL) {
       EX(old_error_reporting) = &EX_T(opline->result.u.var).tmp_var;
   }
   if (EG(error_reporting)) {        zend_alter_ini_entry_ex("error_reporting", sizeof("error_reporting"), "0", 1, ZEND_INI_USER, ZEND_INI_STAGE_RUNTIME, 1);
   }
   ZEND_VM_NEXT_OPCODE();
}
Zend/zend_vm_execute.h
static int ZEND_END_SILENCE_SPEC_TMP_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
   zend_op *opline = EX(opline);
   zval restored_error_reporting;
   if (!EG(error_reporting) && Z_LVAL(EX_T(opline->op1.u.var).tmp_var) != 0) {
       Z_TYPE(restored_error_reporting) = IS_LONG;
       Z_LVAL(restored_error_reporting) = Z_LVAL(EX_T(opline->op1.u.var).tmp_var);        convert_to_string(&restored_error_reporting);
       zend_alter_ini_entry_ex("error_reporting", sizeof("error_reporting"), Z_STRVAL(restored_error_reporting), Z_STRLEN(restored_error_reporting), ZEND_INI_USER, ZEND_INI_STAGE_RUNTIME, 1);
       zendi_zval_dtor(restored_error_reporting);    }
   if (EX(old_error_reporting) == &EX_T(opline->op1.u.var).tmp_var) {
       EX(old_error_reporting) = NULL;
   }
   ZEND_VM_NEXT_OPCODE();
}

二つのhandler間でzend_alter_ini_entry_ex()が呼ばれ、⁠error_reporting⁠を0に設定してエラー表示をoffにし、構文の実行後に⁠error_reporting⁠の値をもとに戻してます。単純にerrorを出力する関数にフラグを渡してエラー出力の抑制を行っているのではなく、このようにhandlerを二つ挟んでいるために遅くなっていることがわかります。

echo '文','字'; (カンマ区切り)のほうが、'文'.'字' (ドット連結)より速い

こちらのtipsについても、はじめに簡単なコードを用いてベンチマークをとってみます。使用するのは'a'、'b'、'c'の3文字を出力するコードです。

benchmark_comma.php
<?php
ob_start();
$t = microtime(true);
while($i < 1000) {
   echo 'a', 'b', 'c';
   ++$i;        
}
$tmp = microtime(true) - $t;
ob_end_clean();

var_dump($tmp);
?>
benchmark_dot.php
<?php
ob_start();
$t = microtime(true);
while($i < 1000) {
   echo 'a' . 'b' . 'c';
   ++$i;        
}
$tmp = microtime(true) - $t;
ob_end_clean();

var_dump($tmp);
?>
$ php benchmark_comma.php
float(0.000305891036987)
$ php benchmark_dot.php
float(0.000324964523315)

実行してみると、ドット連結のほうがわずかですが遅い結果になりました。

これについてもコードを追ってみます。

zend_language_parser.y
536 echo_expr_list:
537         echo_expr_list ',' expr { zend_do_echo(&$3 TSRMLS_CC); }
538     |   expr                    { zend_do_echo(&$1 TSRMLS_CC); }
539 ;

カンマ区切りでは、カンマごとに分解していき、その都度zend_do_echoに渡しているのがわかります。

zend_language_parser.y
582     |   expr '.' expr   { zend_do_binary_op(ZEND_CONCAT, &$$, &$1, &$3 TSRMLS_CC); }

ドット連結の場合は、文字通りですが一度文字の連結を行ってからechoされています。

また定数と定数の連結の場合には下記の関数が呼ばれます。concat_function()関数で連結が行われています。

Zend/zend_vm_execute.h
static int ZEND_CONCAT_SPEC_CONST_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS){
   zend_op *opline = EX(opline);

   concat_function(&EX_T(opline->result.u.var).tmp_var,
       &opline->op1.u.constant,
       &opline->op2.u.constant TSRMLS_CC);


   ZEND_VM_NEXT_OPCODE();
}

 

echo 'a','b','c';
echo 'a'.'b'.'c';

上記のようなコードを例に考えると、カンマの場合'a', 'b', 'c'と順にechoされますが、ドットの場合、定数の連結ではZEND_CONCAT_SPEC_CONST_CONST_HANDLERを呼ばなければならないため、一度すべての文字を連結するためにこのhandlerを呼ぶオーバヘッドが生じると考えられます。

まとめ

PHP最適化tipsについて検証を行いました。あれが速いこれが遅いと言ってきましたが、決してこう書かなければいけないというわけでもありません。コードの読みやすさやメンテナンスのしやすさも同時に重要だと思います。可読性やコードスタイルを考慮してこれらのtipsの導入を考えてください。また、PHPの内部実装について触れるいい機会になると思うのでぜひご活用ください。

次回も引き続き検証を進めるとともに、検証に役立つgdbを用いたPHP内部の動きを追う方法についても紹介したいと思います。

おすすめ記事

記事・ニュース一覧