このページの本文へ

知っていますか? あなたの書いたPHPのコードが実行される4つのプロセス

2017年02月20日 04時00分更新

文●Thomas Punt

  • この記事をはてなブックマークに追加
本文印刷
WordPressをはじめ、Web開発で広く使われているPHP。Webエンジニアなら最低限知っておきたい、その実行プロセスを解説。

最近書かれた『Lexers, Parsers, and ASTs, OH MY!: How Ruby Executes』という記事に刺激を受けて、PHPコードの実行プロセスについて記事を書くことにしました。

Flowchart vector image

はじめに

ほんのわずかなPHPコードを実行する場合でも、内部では多くの処理がされています。大まかにいって、PHPインタプリタは、次の4つのステージでコードを実行します。

  1. 字句解析(Lexing)
  2. 構文解析(Parsing)
  3. コンパイル(Compilation)
  4. 実行(Interpretation)

この記事では、4つのステージにざっと目を通し、各ステージのアウトプットを確認する方法を説明しながら、どのようなことが起きているのかを確かめていきます。使用する拡張モジュールの一部(tokenizerやOPcache)はPHPをインストールする際に標準で組み込まれていますが、そのほかの拡張モジュール(php-astやVLD)は手動でインストールし有効化する必要があります。

ステージ1:字句解析

字句解析(もしくはトークン化)は文字列(記事の場合はPHPソースコード)をトークンの列に変換するプロセスです。トークンとは、マッチした値を表すための、名前の付いた識別子です。PHPはre2cを使用して、zend_language_scanner.l定義ファイルからレキサー(字句解析器)を生成します。

tokenizer拡張モジュールを利用して、字句解析ステージのアウトプットが確認できます。

$code = <<<'code'
<?php
$a = 1;
code;

$tokens = token_get_all($code);

foreach ($tokens as $token) {
    if (is_array($token)) {
        echo "Line {$token[2]}: ", token_name($token[0]), " ('{$token[1]}')", PHP_EOL;
    } else {
        var_dump($token);
    }
}

アウトプットは次のようになります。

Line 1: T_OPEN_TAG ('<?php
')
Line 2: T_VARIABLE ('$a')
Line 2: T_WHITESPACE (' ')
string(1) "="
Line 2: T_WHITESPACE (' ')
Line 2: T_LNUMBER ('1')
string(1) ";"

アウトプットには、特筆すべき点が2つあります。1つは、ソースコードすべてが名前のあるトークンではないということです。代わりに、シンボルの一部(=;:?など)はそれ自体がトークンと見なされています

もう1つは、実はレキサーは単純にトークンの列を出力しているだけではないということです。ほとんどの場合、レキサーは字句(トークンとマッチした値)およびマッチしたトークンのライン番号(スタックトレースなどに用いられます)も保存します。

ステージ2:構文解析

同様に、パーサー(構文解析器)はBisonを使用してBNF grammar fileから生成されます。PHPはLALR(1)(look ahead、left-to-right)文脈自由文法を使用しています。「look ahead(先読み)」とは、パーサーが構文解析中に生じるあいまいさを解決するために、n個のトークン(この場合は1)を先読みできることを示しています。「left-to-right(左から右へ)」は、パーサーがトークンの列を左から右に構文解析することを意味します。

生成されたパーサーはこのステージで、トークンの列をインプットとしてレキサーから受け取ったあと、2つの役割を担います。1つは、トークンがBNF grammar fileに定義された文法ルールのいずれかと一致することを検証し、トークンの順序が有効かどうかを確かめます。同時に、トークンの列の各トークンが有効な言語構成要素を形成していることも確認します。

もう1つの役割は、次のステージ(コンパイル)で使用する、ソースコードを木構造で表現したAST(abstract syntax tree:抽象構文木)を生成することです。

php-ast拡張モジュールを使用することで、パーサーが生成するASTの構造を見られます。内部ASTは「クリーン」ではない(一貫性や全体的な使いやすさの点で)ため、php-ast拡張モジュールはASTをそのまま外部に表示するのではなく、少し変形を加えることでより扱いやすい形にしています。

初歩的なコードのASTを見てみます。

$code = <<<'code'
<?php
$a = 1;
code;

print_r(ast\parse_code($code, 30));

アウトプットは次のようになります。

ast\Node Object (
    [kind] => 132
    [flags] => 0
    [lineno] => 1
    [children] => Array (
        [0] => ast\Node Object (
            [kind] => 517
            [flags] => 0
            [lineno] => 2
            [children] => Array (
                [var] => ast\Node Object (
                    [kind] => 256
                    [flags] => 0
                    [lineno] => 2
                    [children] => Array (
                        [name] => a
                    )
                )
                [expr] => 1
            )
        )
    )
)

木(木構造で表現したAST)のノード(通常はast\Node)はプロパティをいくつか持っています。

  • kind:ノードの種類を表す整数値で、それぞれの値が対応する定数を持つ(例:AST_STMT_LIST => 132,AST_ASSIGN => 517,AST_VAR => 256)
  • flags:オーバーロード動作を示すための整数値 (例:ast\AST_BINARY_OPノードはどの二項演算をしているか区別するフラグを持つ)
  • lineno:上の、トークン情報であるライン番号
  • children:サブノードであり、通常は、さらに分解されるノードの集まり(例:functionノードはparameters、return type、bodyなどのchildrenを持つ)

このステージのアウトプット、ASTは、静的コード解析ツール(例:Phan)などを利用するときに役立ちます。

ステージ3:コンパイル

コンパイルのステージではASTを使用し、木(木構造で表現したAST)を再帰的に走査してオペコードを出力します。また、このステージでは最適化を実行します。最適化には、引数がリテラル値の関数呼び出しの一部を変更すること(例:strlen("abc")int(3)に変更する)や数式の定数畳み込み(例:60 * 60 * 24int(86400)に変更する)が含まれています。

オペコードのアウトプットは、OPcacheVLDPHPDBGなど、さまざまな方法で見られます。記事ではアウトプットが分かりやすいVLDを使用します。

次のスクリプト(file.php)のアウトプットを見てみます。

if (PHP_VERSION === '7.1.0-dev') {
    echo 'Yay', PHP_EOL;
}

次のコマンドを実行します。

php -dopcache.enable_cli=1 -dopcache.optimization_level=0 -dvld.active=1 -dvld.execute=0 file.php

アウトプットは次のようになります。

line     #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------
   3     0  E > > JMPZ                                                     <true>, ->3
   4     1    >   ECHO                                                     'Yay'
         2        ECHO                                                     '%0A'
   7     3    > > RETURN                                                   1

このオペコードはオリジナルのソースコードとある程度似ているため、基本的な処理の流れを追うには十分です(オペコードを詳細に説明すると、それだけで記事が何本も書けてしまうので、この記事ではそこまでするつもりはありません)。上のスクリプトでは最適化をしませんでしたが、見てのとおり、PHP_VERSION === '7.1.0-dev'trueになっているなど、条件が常に変わらない部分にはコンパイルフェーズで変更が加えられています。

OPcacheは、オペコードをキャッシュする(字句解析、構文解析、コンパイルの各ステージをバイパス可能になります)だけではありません。OPcacheにはさまざまなレベルの最適化も用意されています。最適化レベルを4パスに上げて結果を見てみます。

コマンドです。

php -dopcache.enable_cli=1 -dopcache.optimization_level=1111 -dvld.active=-1 -dvld.execute=0 file.php

アウトプットは次のようになります。

line     #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------
   4     0  E >   ECHO                                                     'Yay%0A'
   7     1      > RETURN                                                   1

条件が常に変わらない部分は除かれ、2つのECHO命令が1つの命令にまとめられています。これらは、スクリプトのオペコードに対し(最適化)パスを実行する際にOPcacheが適用するさまざまな最適化のほんの一部にすぎません。各最適化レベルについて説明する場合も記事が1つ書けてしまうので、ここまでにしておきます。

ステージ4:実行

最後のステージはオペコードの実行です。Zend Engine(ZE)VM上でオペコードが実行されるのがこのステージです。このステージは、少ししか説明することがありません(少なくとも、マクロな視点では)。PHPスクリプトがechoprintvar_dumpといった命令によって出力されるあらゆるものがこのステージのアウトプットです。

そこで、このステージではなにか複雑なことを詳説するのではなく、興味深い事実を1つ紹介します。PHPはVMを生成するとき、自分自身に依存しなければならないのです。VMはPHPスクリプトによって生成されるということが理由なのですが、そうすることで、よりシンプルに書け、メンテナンスもより容易になります。

最後に

PHPコードを実行する際に、PHPインタプリタが通過する4つのステージを簡単に説明してきました。各ステージのアウトプットを処理、検証するために、さまざまな拡張モジュール(tokenizer、php-ast、OPcache、VLDなど)を使う必要がありました。

この記事が、PHPインタプリタの全体像をより良く理解するために役立ち、OPcache拡張モジュールの重要性(キャッシュと最適化能力の両方について)が伝われば幸いです。

※本記事はYounes Rafieが査読を担当しています。最高のコンテンツに仕上げるために尽力してくれたSitePointの査読担当者のみなさんに感謝します。

(原文:How PHP Executes – from Source Code to Render

[翻訳:薮田佳佑/編集:Livit

Web Professionalトップへ

WebProfessional 新着記事