PHP

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

2017/02/20

Thomas Punt

48

Articles in this issue reproduced from SitePoint
Copyright © 2017, All rights reserved. SitePoint Pty Ltd. www.sitepoint.com. Translation copyright © 2017, KADOKAWA ASCII Research Laboratories, Inc. Japanese syndication rights arranged with SitePoint Pty Ltd, Collingwood, Victoria,Australia through Tuttle-Mori Agency, Inc., Tokyo

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

Copyright © 2017, Thomas Punt All Rights Reserved.

Thomas Punt

Thomas Punt

イギリスのWebテクノロジーの学校を卒業しました。情熱をプログラミングに注ぎ、特にサーバーサイドのWeb開発技術(中でもPHPおよびElixir)に注力しています。余暇を利用してPHPやその他のオープンソースプロジェクトに参加しているほか、自ら発見した興味深いトピックについてのライティング活動もしています。

Loading...