つい最近、PHP 関係者が依然として一重引用符と二重引用符について話しており、一重引用符の使用は単なる微細な最適化であるが、一重引用符を常に使用することに慣れていれば、大量の CPU を節約できるということを再び聞きました。サイクル!
「すべてはすでに言われていますが、まだ誰もが言っているわけではありません」 – カール・ヴァレンティン
私はこの精神に基づいて、ニキータ・ポポフがすでに 12 年前に書いたのと同じテーマについて記事を書いています (彼の記事を読んでいる場合は、ここで読むのをやめても構いません)。
PHP は文字列補間を実行します。これにより、文字列内で使用されている変数が検索され、使用されている変数の値に置き換えられます。
$juice = "apple"; echo "They drank some $juice juice."; // will output: They drank some apple juice.
この機能は、二重引用符で囲まれた文字列およびヒアドキュメントに限定されます。一重引用符 (または nowdoc) を使用すると、別の結果が得られます:
$juice = "apple"; echo 'They drank some $juice juice.'; // will output: They drank some $juice juice.
見てください: PHP はその一重引用符で囲まれた文字列内の変数を検索しません。したがって、どこでも一重引用符を使用し始めることができます。そこで人々は次のような変更を提案し始めました ..
- $juice = "apple"; $juice = 'apple';
.. より速くなり、PHP は一重引用符で囲まれた文字列内の変数を検索しないため (この例には存在しません)、そのコードを実行するたびに大量の CPU サイクルを節約できるからです。みんな幸せです、事件は解決しました。
一重引用符の使用と二重引用符の使用には明らかに違いがありますが、何が起こっているのかを理解するには、もう少し深く掘り下げる必要があります。
PHP はインタープリター型言語ですが、仮想マシンが実際に実行できるもの (オペコード) を取得するために特定の部分が連携するコンパイル ステップを使用しています。では、PHP ソース コードからオペコードにどうやってアクセスするのでしょうか?
レクサーはソース コード ファイルをスキャンし、トークンに分割します。これが何を意味するのかについての簡単な例は、token_get_all() 関数のドキュメントに記載されています。
T_OPEN_TAG (この 3v4l.org スニペットで実際の動作を確認し、試してみることができます。
パーサー
パーサーはこれらのトークンを受け取り、そこから抽象構文ツリーを生成します。上記の例の AST 表現を JSON として表すと次のようになります:
{ "data": [ { "nodeType": "Stmt_Echo", "attributes": { "startLine": 1, "startTokenPos": 1, "startFilePos": 6, "endLine": 1, "endTokenPos": 4, "endFilePos": 13 }, "exprs": [ { "nodeType": "Scalar_String", "attributes": { "startLine": 1, "startTokenPos": 3, "startFilePos": 11, "endLine": 1, "endTokenPos": 3, "endFilePos": 12, "kind": 2, "rawValue": "\"\"" }, "value": "" } ] } ] }これも試して、他のコードの AST がどのように見えるかを確認したい場合は、Ryan Chandler による https://phpast.com/ と https://php-ast-viewer.com/ を見つけました。どちらも、特定の PHP コードの AST を表示します。
コンパイラ
コンパイラは AST を取得し、オペコードを作成します。オペコードは仮想マシンが実行するものであり、OPcache を設定して有効にしている場合 (これを強くお勧めします)、OPcache に保存されるものでもあります。
オペコードを表示するには、複数のオプションがあります (おそらくもっとあるかもしれませんが、私が知っているのはこれら 3 つです):
- vulcan ロジック ダンパー拡張機能を使用します。 3v4l.org にも焼き付けられています
- phpdbg -p script.php を使用してオペコードをダンプします
- または、OPcache の opcache.opt_debug_level INI 設定を使用して、オペコードを出力させる
- 値 0x10000 は最適化前のオペコードを出力します
- 値 0x20000 は、最適化後にオペコードを出力します。
$ echo ' foo.php $ php -dopcache.opt_debug_level=0x10000 foo.php $_main: ... 0000 ECHO string("") 0001 RETURN int(1)仮説
一重引用符と二重引用符を使用するときに CPU サイクルを節約するという最初のアイデアに戻ると、これが成り立つのは、PHP が単一のリクエストごとに実行時にこれらの文字列を評価する場合のみであるということに誰もが同意すると思います。
実行時に何が起こるのでしょうか?
それでは、PHP が 2 つの異なるバージョンに対してどのオペコードを作成するかを見てみましょう。
二重引用符:
0000 ECHO string("apple") 0001 RETURN int(1)vs.一重引用符:
0000 ECHO string("apple") 0001 RETURN int(1)ちょっと待って、何か奇妙なことが起こりました。これは同じに見えます!私のマイクロ最適化はどこへ行ったのでしょうか?
まあ、おそらく、ECHO オペコード ハンドラーの実装が指定された文字列を解析する可能性がありますが、そうするように指示するマーカーやその他の何かはありません...うーん?
別のアプローチを試して、これら 2 つのケースに対してレクサーが何を行うかを見てみましょう:
二重引用符:
T_OPEN_TAG (vs.一重引用符:
Line 1: T_OPEN_TAG (トークンは依然として二重引用符と一重引用符を区別していますが、AST をチェックすると、どちらの場合でも同じ結果が得られます。唯一の違いは、Scalar_String ノード属性の rawValue であり、依然として一重引用符と二重引用符が含まれていますが、値にはどちらの場合も二重引用符が使用されます。
新しい仮説
文字列補間は実際にはコンパイル時に行われる可能性はありますか?
もう少し「洗練された」例で確認してみましょう:
このファイルのトークンは次のとおりです:
T_OPEN_TAG (最後の 2 つのトークンを見てください。文字列補間はレクサーで処理されるため、コンパイル時の処理であり、実行時とは何の関係もありません。
完全を期すために、これによって生成されたオペコードを見てみましょう (最適化後、0x20000 を使用):0000 ASSIGN CV0($juice) string("apple") 0001 T2 = FAST_CONCAT 文字列("ジュース: ") CV0($ジュース) 0002 エコーT2 0003 リターン int(1)
0000 ASSIGN CV0($juice) string("apple") 0001 T2 = FAST_CONCAT string("juice: ") CV0($juice) 0002 ECHO T2 0003 RETURN int(1)これは、単純な 要点を説明します。連結するか補間する必要がありますか?これら 3 つの異なるバージョンを見てみましょう:
0000 ASSIGN CV0($juice) string("apple") 0001 T2 = FAST_CONCAT string("juice: ") CV0($juice) 0002 ECHO T2 0003 RETURN int(1)
に割り当てます。
0000 ASSIGN CV0($juice) string("apple") 0001 T2 = FAST_CONCAT string("juice: ") CV0($juice) 0002 ECHO T2 0003 RETURN int(1)最初のバージョン (文字列補間) は、基礎となるデータ構造としてロープを使用しており、文字列のコピーをできるだけ少なくするように最適化されています。
0000 ASSIGN CV0($juice) string("apple") 0001 T2 = FAST_CONCAT string("juice: ") CV0($juice) 0002 ECHO T2 0003 RETURN int(1)2 番目のバージョンは、中間の文字列表現を作成しないため、最もメモリ効率が高くなります。代わりに、I/O の観点からはブロック呼び出しとなる ECHO への呼び出しを複数回実行するため、ユースケースによってはこれが欠点になる可能性があります。
0000 ASSIGN CV0($juice) string("apple") 0001 T2 = FAST_CONCAT string("juice: ") CV0($juice) 0002 ECHO T2 0003 RETURN int(1)3 番目のバージョンは、CONCAT/FAST_CONCAT を使用して中間文字列表現を作成するため、ロープ バージョンよりも多くのメモリを使用する可能性があります。
0000 ASSIGN CV0($juice) string("apple") 0001 T2 = FAST_CONCAT string("juice: ") CV0($juice) 0002 ECHO T2 0003 RETURN int(1)それでは...ここで行うべき正しいことは何ですか?なぜ文字列補間なのでしょうか?
文字列補間では、echo "juice: $juice" の場合は FAST_CONCAT のいずれかを使用します。または、echo "juice: $juice $juice" の場合は高度に最適化された ROPE_* オペコードですが、最も重要なのは、意図を明確に伝えており、これが私がこれまでに使用した PHP アプリケーションのいずれにおいてもボトルネックになったことはありません。したがって、実際にはどれも重要ではありません。
TLDR
ただし、注意点が 1 つあります。PHP 4 まで (5.0 や 5.1 も含まれていると思いますが、わかりません) は実行時に文字列補間を行っていたため、これらのバージョンを使用すると... うーん、おそらく実際にまだ PHP 5 を使用している人はいます。上記と同じことが当てはまります。問題は二重引用符で囲まれた文字列ではなく、古い PHP バージョンの使用です。
最終アドバイス
免責事項: 提供されるすべてのリソースの一部はインターネットからのものです。お客様の著作権またはその他の権利および利益の侵害がある場合は、詳細な理由を説明し、著作権または権利および利益の証拠を提出して、電子メール [email protected] に送信してください。 できるだけ早く対応させていただきます。
Copyright© 2022 湘ICP备2022001581号-3