php8.0から新しい制御構文であるmatch式が追加されました。
https://www.php.net/manual/ja/control-structures.match.php
公式ドキュメントにもある通り、match式は基本的にはswitch文と似た振る舞いをしますが、以下のように違う点も存在します。
match 式の比較は、 switch 文が行う弱い比較ではなく、 厳密に値を比較(===) します。
match 式は値を返します。
match 式の分岐は、 switch 文のように後の分岐に抜けたりはしません。
match 式は、全ての場合を網羅していなければいけません。
さて、そんなmatch式が内部では何を行っているのかを調べていきます。
まず事前知識としてmatchと似た挙動をするswitch文の挙動を確認するため、簡単なswitch文のオペコードを見てみます。
1 2 3 4 5 6 7 8 9 10 |
<?php $c = 0; $r; switch ( $c ) { case 0: $r = 'a'; break; case 1: $r = 'b'; break; default:$r = 'c'; break; } |
↓↓↓
1 2 3 4 5 6 7 8 9 10 11 12 13 |
0000 ASSIGN CV0($c) int(0) 0001 T3 = IS_EQUAL CV0($c) int(0) 0002 JMPNZ T3 0006 0003 T3 = IS_EQUAL CV0($c) int(1) 0004 JMPNZ T3 0008 0005 JMP 0010 0006 ASSIGN CV1($r) string("a") 0007 JMP 0012 0008 ASSIGN CV1($r) string("b") 0009 JMP 0012 0010 ASSIGN CV1($r) string("c") 0011 JMP 0012 0012 RETURN int(1) |
処理としては単純で、switchに与えられた値と各caseの値で上から順に曖昧な比較(IS_EQUAL)を行い、
一致したものがあれば当該処理の行までジャンプ(JMPNZ)し、処理が終わればさらにswitchの終わりまでジャンプ(JMP)するという処理です。
このIS_EQUALというオペコードはphpの==とほぼ同等の意味を持ちます。
https://www.php.net/manual/ro/internals2.opcodes.is-equal.php
つまり、switch文は内部的な処理で見るとif文の繰り返しと同じような挙動ということです。
phpのswitch文は後の方に書かれたcaseの処理時間が長くなるという話をよく聞きますが、これは処理に至るまでの比較回数が多いことから起きる現象です。
(変数$cが入力値でなく定数だった場合は、最適化され処理時間が同じになるケースが多いです。)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
<?php $c = $argv[1]; $r = ''; $t_s = microtime(true); for ($i = 0; $i < 1_0000_0000; $i++) { switch ( $c ) { case 0: $r = 'a'; break; case 1: $r = 'b'; break; case 2: $r = 'c'; break; case 3: $r = 'd'; break; case 4: $r = 'e'; break; default:$r = 'z'; break; } } $t_r = microtime(true) - $t_s; echo "time : ".$t_r."\n"; /* $ php test.php 0 -> time : 2.737939119339 $ php test.php 4 -> time : 13.107921123505 */ |
では本題のmatch式を詳しく見ていきます。switchと同じように、とりあえずオペコードを表示してみましょう。
1 2 3 4 5 6 7 8 9 10 |
<?php $c = 0; $r; match ( $c ) { 0 => $r = 'a', 1 => $r = 'b', default => $r = 'c', }; |
↓↓↓
1 2 3 4 5 6 7 8 9 10 11 12 13 |
0000 ASSIGN CV0($c) int(0) 0001 MATCH CV0($c) 0: 0002, 1: 0005, default: 0008 0002 T4 = ASSIGN CV1($r) string("a") 0003 T5 = QM_ASSIGN T4 0004 JMP 0011 0005 T6 = ASSIGN CV1($r) string("b") 0006 T5 = QM_ASSIGN T6 0007 JMP 0011 0008 T7 = ASSIGN CV1($r) string("c") 0009 T5 = QM_ASSIGN T7 0010 JMP 0011 0011 FREE T5 0012 RETURN int(1) |
個人的には、てっきり曖昧な比較の箇所が厳密な比較に変わり、一時変数が返されるような処理が来るかと思っていましたが違いました。
0001に見慣れないMATCHというオペコードがありますが、これがmatch式そのもののようです。
php公式のオペコード一覧にも載っていなかったのでphp8.0で追加されたんですかね?(2021/8/4)
https://www.php.net/manual/ro/internals2.opcodes.php
このMATCHというオペコードの挙動を確認するためにはphpのソースを読んでいくしかないですが、C言語はだいぶ苦手なのでざっくり雰囲気で書いていきます。
今回見ていくソースはphp-8.0.8のブランチになります。( https://github.com/php/php-src/tree/PHP-8.0.8 )
とりあえず本当にMATCHというオペコードが存在するのか不明なので、一覧が記載されているzend_vm_opcodes.hを確認します。
https://github.com/php/php-src/blob/PHP-8.0.8/Zend/zend_vm_opcodes.h#LC280
280 : #define ZEND_MATCH 195
存在はするようですね。
ここからどう追っていけばいいのか怪しいですが、vm(バーチャルマシン)をexecute(実行)などと書いてあるファイルがあったので、多分これですね。
きっとここに処理が書いてあるはずです。(雰囲気)
https://github.com/php/php-src/blob/PHP-8.0.8/Zend/zend_vm_execute.h#LC7396
ZEND_MATCHでファイル内を検索してみると7396行目にZEND_MATCH_SPEC_CONST_CONST_HANDLERという名前の関数が見つかりました。
12472行目にも似たような関数(ZEND_MATCH_SPEC_TMPVARCV_CONST_HANDLER)があるのですが、この2つの違いは何なんでしょうね……
このあたり詳しくないのですが、中身の処理にはあまり違いが無いので今は無視します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
static ZEND_VM_COLD ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_MATCH_SPEC_CONST_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS) { USE_OPLINE zval *op, *jump_zv; HashTable *jumptable; op = RT_CONSTANT(opline, opline->op1); jumptable = Z_ARRVAL_P(RT_CONSTANT(opline, opline->op2)); match_try_again: if (Z_TYPE_P(op) == IS_LONG) { jump_zv = zend_hash_index_find(jumptable, Z_LVAL_P(op)); } else if (Z_TYPE_P(op) == IS_STRING) { jump_zv = zend_hash_find_ex(jumptable, Z_STR_P(op), IS_CONST == IS_CONST); } else if (Z_TYPE_P(op) == IS_REFERENCE) { op = Z_REFVAL_P(op); goto match_try_again; } else { if (UNEXPECTED((IS_CONST & IS_CV) && Z_TYPE_P(op) == IS_UNDEF)) { SAVE_OPLINE(); op = ZVAL_UNDEFINED_OP1(); if (UNEXPECTED(EG(exception))) { HANDLE_EXCEPTION(); } goto match_try_again; } goto default_branch; } if (jump_zv != NULL) { ZEND_VM_SET_RELATIVE_OPCODE(opline, Z_LVAL_P(jump_zv)); ZEND_VM_CONTINUE(); } else { default_branch: /* default */ ZEND_VM_SET_RELATIVE_OPCODE(opline, opline->extended_value); ZEND_VM_CONTINUE(); } } |
処理を見ていくと、7406行目あたりにif文があります。
zvalから型を取り出し、数値(IS_LONG)ならばzend_hash_index_find()、文字列(IS_STRING)ならばzend_hash_find_ex()、
参照(IS_REFERENCE)ならば参照先の型に対応したいずれかを呼び出しています。
それらに該当しない場合は例外チェックをした後にdefaultと解釈され、goto文でdefault_branchタグへ飛ばされているようです。
この時点で、defaultかどうかが判別されている為、switchのように後回しになるようなことは無さそうですね。
分岐後の関数ですが、zend_hash_find_ex()の方を辿っていくとzend_hash_find_bucket()に行きつきます。
https://github.com/php/php-src/blob/PHP-8.0.8/Zend/zend_hash.c#LC639
なかなか処理を追うのが厳しいですが、zend_hash_find_bucketで調べてみるとこんなことを書いている方がいました。
https://blog.katastros.com/a?ID=01600-322053ef-8c2c-45ed-afc3-ef12eeaccfa5
要はzend_hash_find_bucket()はハッシュテーブルの検索処理らしいです。関数名からしても何となく”らしさ”はあります。
zend_hash_find_bucket()はmatch実装で追加されたわけではなく、もともと配列関連の処理で使われているのだとか。
つまりmatchはswitchのような単純な比較を繰り返すのではなく、テーブルを使用して飛ぶべき箇所を決定していたようです。
確かに比較を何度も行うよりも明らかに早いですし、他の実装も良い感じに再利用されているため環境に優しそうですね。
処理速度も計測してみましたが、やはり「後のcaseは時間が掛かる」なんてことはありません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
<?php $c = $argv[1]; $r = ''; $t_s = microtime(true); for ($i = 0; $i < 1_0000_0000; $i++) { match ( $c ) { 0 => $r = 'a', 1 => $r = 'b', 2 => $r = 'c', 3 => $r = 'd', 4 => $r = 'e', default => $r = 'z', }; } $t_r = microtime(true) - $t_s; echo "time : ".$t_r."\n"; /* $ php test.php 0 -> time : 1.2953310012817 $ php test.php 4 -> time : 1.3015880584717 */ |
今回調べて初めて知ったことですが、C言語のswitch文はphpのmatchのようにテーブルで実装されているようですね。
(なぜC言語で書かれているphpのswitchはif文の重ね掛けになってしまったのか……)
ということで、match式の内部的な挙動はこんな感じのようです。
- switch文とは違いテーブルでの条件分岐を行っている。(どちらかというと連想配列に近い)
- また、テーブルでの条件分岐なので分岐の数が増えても処理が定数時間で済む。
- defaultは他処理とは別の分岐で処理されるため早い。
とはいえ、処理に単一の式のみしか書けない事から多少の歯痒さはあります。
switchと完全に置き換え可能というわけではなさそうですね。
(というのと、残念ながら弊社ではPHPのバージョン的に未実装です。)
おまけ
ちなみに、連想配列も言語仕様上はこんな使い方が(一応は)可能です。
defaultに相当するモノが無いというのと、今回は調べていないので詳しくは分かりませんが、phpの連想配列は要素数に比例(?)して処理時間も増加していくようです。
多少のデメリットはあれど、php8未満の時にmatchを使いたくなったらこれでいいのでは……?
1 2 3 4 5 6 7 8 9 10 11 |
<?php $c = 0; $r; array( 0 => $r = 'a', 1 => $r = 'b', 2 => $r = 'c', )[$c]; echo $r; // a |