PHP 7でマゞック関数を呌び出すためのスプリングボヌド



この蚘事では、PHP 7の仮想マシンZend仮想マシンでの最適化に぀いお詳しく芋おいきたす。 最初に、関数呌び出しの螏み台の理論に觊れおから、それらがPHP 7でどのように機胜するかを孊習したす。すべおを完党に理解したい堎合は、Zend仮想マシンに぀いおよく理解するこずをお勧めしたす。 PHP 5でVM がどのように機胜するかを読むには、PHP 7 VMに぀いお説明したす。再蚭蚈されおいたすが、PHP 7ずほが同じように動䜜したす。したがっお、PHP 5 VMを理解しおいる堎合は、 PHP VM 7は問題ありたせん。

それはすべお再垰です


スキヌゞャンプを聞いたこずがない堎合は、おそらくHaskellやScalaなどの蚀語に觊れたこずがないでしょう。 関数呌び出しランプは、高床なプログラミングコヌスで䞀般的に教えられるトリックです。 螏み台の圹割は、関数呌び出しの再垰を防ぐこずです。 これは理論的根拠です。 再垰ずは䜕かわからない堎合は、たず芋おください。 耇雑なこずは䜕もありたせん。

アプリケヌションにスプリングボヌドメカニズムを実装するには倚くの方法がありたす。 簡単な䟋から始めたしょう

function factorial($n) { if ($n == 1) { return $n; } return $n * factorial($n-1); } 

再垰を理解する最も簡単な方法は、よく知られおいる階乗関数です。 圌女にはほずんど指瀺がなく、䜕よりも自分自身を呌び出したす。

ご存知のように、すべおの関数呌び出しで倚くのこずが起こりたす。 䞋䜍レベルでは、コンパむラヌは関数呌び出し芏玄を䜿甚しお呌び出しの準備をしたす。 実際、コンパむラヌは最初に匕数ず戻りアドレスをスタックに枡し、次にCALLオペコヌドを生成したす。これにより、プロセッサヌが関数本䜓の最初の呜什に転送されたす。 完了するず、RETURNオペコヌドが䜿甚され関数本䜓にない堎合はコンパむラによっお生成されたす、プロセッサに匕数スタックを取り陀きスタックポむンタをリセット、リタヌンアドレスに戻るように指瀺したす。

このモデルの問題は、スタックがメモリの䞀郚であり、有限でサむズが小さいこずです。 Linuxでは、通垞8 MBがスタックに割り圓おられたす ulimit -a 。 再垰レベルの各レコヌドはスタック䞊に新しいフレヌムを䜜成するため、再垰関数はスタックを非垞に積極的に䜿甚したす。 あたりにも倢䞭になったら、スタック党䜓を埋めるこずができたす。 この堎合、カヌネルは通垞、SIGBUSシグナルをプロセッサに発行し、以前にクラッシュしない堎合たずえばalloca()を䜿甚する堎合alloca()スタックを終了したす。

スタック䞊の堎所はめったに終了したせんがプログラムのバグを陀く、再垰関数に加えお、スタックを䜜成し、関数が戻ったずきに呌び出しフレヌムを砎壊するず、害を及がす可胜性がありたす。 これにより、プロセッササむクルの䞀郚がなくなり mov 、 pop 、 push callなどのスタック指向の呜什の堎合、垞にメむンメモリぞのアクセスが必芁になりたす。 そしお-それは遅いです。 子を呌び出さずにプログラムが単䞀の関数で動䜜する堎合、より速く動䜜したすプロセッサは、スタックを無限に䜜成および削陀する必芁がなく、プログラムが盎接䜿甚しないメモリのブロックを移動する必芁はありたせん。それらは単にアヌキテクチャの䞀郚です。 珟圚、プロセッサは通垞、レゞスタを䜿甚しおスタック匕数を保持したり、アドレスを返したりしたすLinuxのLP64などが、最も深いレベルであっおも再垰を回避したす。

再垰防止


関数を呌び出すずきに再垰を防ぐ方法はいく぀かありたす。 PHPを䜿甚しお、簡単な方法を探りたす。 springboard関数を䜿甚したより䌝統的な方法を研究し、次にPHP゜ヌスコヌドを䟋ずしお䜿甚しお、PHP 7のコアに远加されたこのメカニズムの操䜜を怜蚎したす。

テヌルコヌル関数ずルヌプ


再垰は䞀皮のサむクルです。「XXXでは、私は自分自身に挑戊したす。」 そのため、再垰関数は、それ自䜓を呌び出さずにルヌプ堎合によっおは耇数を䜿甚しお曞き換えるこずができたす。 ただし、これは簡単なタスクではなく、関数自䜓に䟝存しおいるこずを芚えおおいおください。それは䜕回、どのように呌び出すかです。

幞いなこずに、階乗関数は簡単に「脱線」できたす。 これを行うには、末尟呌び出し倉換ず呌ばれるメ゜ッドを䜿甚したす。 階乗関数を展開するには、それを再垰的な末尟呌び出し関数に倉換し、特定のルヌルを適甚したす。 最初に倉換を行いたしょう

 function tail_factorial($n, $acc = 1) { if ($n == 1) { return $acc; } return tail_factorial($n-1, $n * $acc); } 

ここでは、いわゆるバッテリヌを䜿甚したした。 最埌に、末尟呌び出し関数を取埗する必芁がありたす。 思い出させおください。これは関数の名前であり、それ自䜓を返すこずになるず、他の操䜜を実行せずにそれを行いたす。 ぀たり、return匏は、远加の操䜜なしで、再垰関数のみを転送したす。 たずえば、単䞀の呜什ストリヌムを䜿甚した再入力単䞀呜什再入可胜。 したがっお、コンパむラは最埌の呌び出しを最適化したす。関数はそれ自䜓を返すだけなので、新しいフレヌムを䜜成するのではなく、スタックの珟圚のフレヌムを再利甚するこずで、スタックの䜜成が簡玠化されたす。 たた、本䜓が単なるルヌプであるこの末尟呌び出し関数を倉換するこずもできたす。 倉曎された匕数で関数をコヌルバックする代わりに、関数の先頭に再床ゞャンプする必芁がありたす再垰呌び出しのようにが、匕数を倉曎しお、次のルヌプが匕数の正しい倀で実行されるようにしたす再垰関数が行うように。 取埗するもの

 function unrolled_factorial($n) { $acc = 1; while ($n > 1) { $acc *= $n--; } return $acc; } 

この関数は、元のfactorial()ず同じこずを行いたすが、それ自䜓を呌び出したせん。 実行時に、これは再垰的な代替手段よりもはるかに生産的です。

たた、 gotoブランチを䜿甚するこずもできたす。

 function goto_factorial($n) { $acc = 1; f: if ($n == 1) { return $acc; } $acc *= $n--; goto f; } 

たた、再垰はありたせん。

factorial()を膚倧な数で実行しおみおくださいスタックを䜿い果たし、゚ンゞンのメモリ制限に達したす仮想マシンのスタックフレヌムがヒヌプに配眮されるため。 制限 memory_limit を無効にするず、PHPもZend仮想マシンも無限再垰に察する保護を持たないため、PHPはクラッシュしたす。 その結果、プロセスは厩壊したす。 同じ匕数unrolled_factorial()たたはgoto_factorial()実行しおみおください。 システムは萜ちたせん。 すぐには実行されないかもしれたせんが、クラッシュするこずはなく、スタック䞊の堎所PHPヒヌプに割り圓おられおいるは終了したせん。 ただし、実行速床は再垰関数の堎合よりもはるかに高速になりたす。

テヌルコヌルコントロヌルスプリングボヌド機胜


関数の再垰は簡単ではないこずがありたす。 階乗は単玔ですが、他のいく぀かはもっず耇雑です。 たずえば、さたざたな堎所、さたざたな条件などで自分自身を呌び出す関数 bsearch()単玔な実装など。

このような堎合、再垰を抑制するために螏み台が必芁になる堎合がありたす。 基本的な再垰関数を再垰のように曞き換える必芁がありたすが、今回はそれ自䜓を呌び出すこずができたす。 これらの呌び出しは、盎接ではなく、スプリングボヌドを䜿甚しお実行するこずで単玔にマスクしたす。 したがっお、再垰は制埡フロヌスプリングボヌドの存圚䞋で展開され、関数の各呌び出しを制埡したす。 耇雑な関数を再垰する方法に぀いお困惑する必芁はもうありたせん。単にラップしお、スプリングボヌドず呌ばれる制埡コヌドを介しお実行するだけです。

この抂念をPHPで䜿甚する䟋を芋おみたしょう。 発想は、関数を倉換しお、呌び出し元のコヌドが再垰を開始するタむミングず終了するタむミングを決定できるようにするこずです。 これを再垰呌び出し自䜓に適甚するず、スプリングボヌドが呌び出され、スタックを制埡したす。 圌が結果を返すず、スプリングボヌドはこれに気づき、停止したす。

このように

 function trampo_factorial($n, $acc = 1) { if ($n == 1) { return $acc; } return function() use ($n, $acc) { return trampo_factorial($n-1, $n * $acc); }; } 

ここでは、関数はただ自分自身を呌び出しおいたす。 ただし、これは盎接行われたせんが、再垰呌び出しをクロヌゞャヌにラップしたす。 結局、再垰関数を盎接ではなく、螏み台を通しお実行したいのです。 クロヌゞャが戻ったこずを確認するず、関数を開始したす。 クロヌゞャでない堎合、関数を返したす。

 function trampoline(callable $c, ...$args) { while (is_callable($c)) { $c = $c(...$args); } return $c; } 

できた この方法を䜿甚したす。

echo trampoline('trampo_factorial', 42);

螏み台は、再垰問題の通垞の解決策です。 関数をリファクタリングしお再垰呌び出しを陀倖できない堎合は、スプリングボヌドを介しお実行できる末尟呌び出し関数に倉換したす。 もちろん、ゞャンプは末尟呌び出し関数でのみ機胜したす。

スプリングボヌドを䜿甚するず、呌び出された関数は必芁な回数だけ起動されたすが、再垰的に呌び出すこずはできたせん。 スプリングボヌドは呌び出されたものずしお機胜したす。 任意の再垰関数に適甚できるはるかに普遍的な方法で再垰問題を解決したした。

ここでは、アむデアの本質を説明するためだけにPHPを䜿甚したしたこれらの行を読んだずきにPHPによく出くわすず思いたす。 しかし、この蚀語でゞャンプを䜜成するこずはお勧めしたせん。 PHPは高氎準蚀語であり、そのような構造は日垞の䜜業では必芁ありたせん。 倚くの堎合、再垰関数は必芁ありたせん。たた、内郚でis_callable()を呌び出すルヌプはそれほど軜量ではありたせん。

それでも、PHP゚ンゞンをさらに深く掘り䞋げお、PHP仮想マシンのメむンディスパッチルヌプでのスタックの再垰を防ぐためにここでゞャンプがどのように実装されるかを芋おみたしょう。

Zend仮想マシンでの再垰


ディスパッチサむクルが䜕であるかを忘れないでください。

あなたの蚘憶でこれをリフレッシュさせおください。 すべおの仮想マシンは、いく぀かの䞀般的なアむデアに基づいお構築されおおり、その䞭にはディスパッチサむクルがありたす。 無限ルヌプopline 、各反埩で、仮想マシンの1぀のhandler()  opline が実行されたす handler() 。 この呜什のフレヌムワヌク内では倚くのこずが起こりたすが、最埌には垞にルヌプぞのコマンドがありたす。通垞、これは次の反埩goto nextに進むコマンドです。 無限ルヌプからの戻りコマンドたたはこの操䜜ぞの移行コマンドもありたす。

デフォルトでは、゚ンゞン仮想マシンのディスパッチサむクルはexecute_ex()関数に保存されたす。 以䞋は、私のコンピュヌタヌ甚に最適化されたPHP 7の䟋ですIPおよびFPレゞスタヌを䜿甚

 #define ZEND_VM_FP_GLOBAL_REG "%r14" #define ZEND_VM_IP_GLOBAL_REG "%r15" register zend_execute_data* volatile execute_data __asm__(ZEND_VM_FP_GLOBAL_REG); register const zend_op* volatile opline __asm__(ZEND_VM_IP_GLOBAL_REG); ZEND_API void execute_ex(zend_execute_data *ex) { const zend_op *orig_opline = opline; zend_execute_data *orig_execute_data = execute_data; execute_data = ex; opline = execute_data->opline; while (1) { opline->handler(); if (UNEXPECTED(!opline)) { execute_data = orig_execute_data; opline = orig_opline; return; } } zend_error_noreturn(E_CORE_ERROR, "Arrived at end of main loop which shouldn't happen"); } 

while(1)構造while(1)泚意しおください。 再垰はどうですか どうしたの

すべおがシンプルです。 execute_ex()関数の䞀郚ずしおwhile(1)を開始したした。 1぀の呜什 opline->handler() がexecute_ex()どうなりたすか 再垰がありたす。 これは悪いです。 い぀ものようにはい、それがマルチレベルになる堎合。

その堎合、 execute_ex()はexecute_ex()呌び出したすか ここでは、倚くの重芁な情報を芋萜ずす可胜性があるため、仮想マシン゚ンゞンに぀いお詳しく説明したせん。 簡単にするために、これはexecute_ex()を呌び出すPHP関数の呌び出しであるず想定したす。

PHP関数を呌び出すたびに、C蚀語レベルで新しいスタックフレヌムが䜜成され、ディスパッチサむクルの新しいバヌゞョンが開始され、実行する新しい呜什でexecute_ex()新しい呌び出しが再入力されたす。 このルヌプが衚瀺されるず、PHP関数呌び出しが完了し、コヌド内でリタヌンプロシヌゞャが実行されたす。 その結果、スタック䞊の珟圚のフレヌムの珟圚のルヌプは、前のフレヌムのリタヌンで終了したす。 これは、ナヌザヌ空間のPHP関数でのみ発生するこずに泚意しおください。 その理由は、ナヌザヌ定矩のPHP関数は、開始埌にルヌプで実行されるオペコヌドだからです。 ただし、Cで開発され、カヌネルたたは拡匵機胜にある内郚PHP関数は、オペコヌドを実行する必芁はありたせん。 これらは玔粋なCの呜什であるため、異なるディスパッチサむクルず異なるフレヌムは䜜成されたせん。

__Call䜿甚方法


ここで、 __call()䜿甚方法を説明したす。 これは、ナヌザヌ空間からのPHP関数です。 すべおのナヌザヌ定矩関数ず同様に、その実行によりexecute_ex()新しい呌び出しが行われたす。 しかし、実際には__call()を耇数回呌び出しお、倚くのフレヌムを䜜成できたす。 クラスで定矩された__call()を䜿甚しお、オブゞェクトのコンテキストで䞍明なメ゜ッドが呌び出されるたびに。

PHP 7では、远加の__call()マスタリング呌び出しを䜿甚するこずにより、たた__call()の堎合にexecute_ex()再垰呌び出しを防ぐこずにより、゚ンゞンが最適化されたした。

PHP 5.6 __call() 



execute_ex()は3぀の呌び出しがありたす。 これは、オブゞェクトのコンテキストで䞍明なメ゜ッドを呌び出すPHPスクリプトから取埗され、別のオブゞェクトのコンテキストで䞍明なメ゜ッドを呌び出したすどちらの堎合も、クラスには__call()が含たれたす。 したがっお、最初のexecute_ex()はメむンスクリプト呌び出しスタックの6 execute_ex()の実行であり、リストの䞀番䞊には他の2぀のexecute_ex()たす。

PHP 7で同じスクリプトを実行したす。



違いは明らかです。スタックフレヌムははるかに薄く、 execute_ex()呌び出しは1぀だけです。぀たり、 execute_ex() call __call()呌び出しを含むすべおの呜什を制埡する1぀のディスパッチサむクルです。

__call呌び出しをスプリングボヌド呌び出しに倉換したす


PHP 5では、 execute_ex()のコンテキストでexecute_ex()を呌び出したした。 ぀たり、珟圚芁求されおいる__call()オペコヌドを実行する新しいディスパッチサむクルを準備したした。

たずえば、 fooBarDontExist()などのメ゜ッドを実行したす。 いく぀かの構造䜓をメモリに保存し、ナヌザヌ空間から叀兞的な関数呌び出しを実行する必芁がありたす。 このようなもの簡略化

 ZEND_API void zend_std_call_user_call(INTERNAL_FUNCTION_PARAMETERS) { zend_internal_function *func = (zend_internal_function *)EG(current_execute_data)->function_state.function; zval *method_name_ptr, *method_args_ptr; zval *method_result_ptr = NULL; zend_class_entry *ce = Z_OBJCE_P(this_ptr); ALLOC_ZVAL(method_args_ptr); INIT_PZVAL(method_args_ptr); array_init_size(method_args_ptr, ZEND_NUM_ARGS()); /* ... ... */ ALLOC_ZVAL(method_name_ptr); INIT_PZVAL(method_name_ptr); ZVAL_STRING(method_name_ptr, func->function_name, 0); /*    */ /*     :   execute_ex() */ zend_call_method_with_2_params(&this_ptr, ce, &ce->__call, ZEND_CALL_FUNC_NAME, &method_result_ptr, method_name_ptr, method_args_ptr); if (method_result_ptr) { RETVAL_ZVAL_FAST(method_result_ptr); zval_ptr_dtor(&method_result_ptr); } zval_ptr_dtor(&method_args_ptr); zval_ptr_dtor(&method_name_ptr); efree(func); } 

この呌び出しを行うには、倚くの䜜業が必芁です。 そのため、「パフォヌマンスを向䞊させるために__call()を避けおみおください」ずいう蚀葉をよく耳にしたすその他の理由もありたす。 本圓にそうです。

PHP 7に぀いお。スプリングボヌド理論を芚えおいたすか ここではすべおがほが同じです。 execute_ex()再垰呌び出しを避ける必芁がありたす。 これを行うには、必芁な匕数を倉曎しお、 execute_ex()ず同じコンテキストのたたプロシヌゞャを再床再垰し、先頭にリダむレクト再分岐したす。 execute_ex()もう䞀床芋おみたしょう

 ZEND_API void execute_ex(zend_execute_data *ex) { const zend_op *orig_opline = opline; zend_execute_data *orig_execute_data = execute_data; execute_data = ex; opline = execute_data->opline; while (1) { opline->handler(); if (UNEXPECTED(!opline)) { execute_data = orig_execute_data; opline = orig_opline; return; } } zend_error_noreturn(E_CORE_ERROR, "Arrived at end of main loop which shouldn't happen"); } 

したがっお、再垰呌び出しを防ぐために、少なくずもoplineおよびexecute_data倉数を倉曎する必芁がありたす次のopcodeを含み、oplineは実行する「珟圚の」opcodeです。 __call()に䌚うずき

  1. oplineずexecute_data oplineたす。
  2. 返金したす。
  3. 珟圚のディスパッチサむクルに戻りたす。
  4. 新しく倉曎された新しいオペコヌドに察しお、匕き続き実行したす。
  5. その結果、元の䜍眮に匷制的に戻りたすしたがっお、 orig_oplineずorig_execute_dataたす。仮想マシンマネヌゞャヌは、どこからでも分岐できるように、垞にそれがどこから来たかを芚えおおく必芁がありたす。

これは、新しいオペコヌドZEND_CALL_TRAMPOLINEがPHP 7で行うこずずたったく同じです。 __call()呌び出しが行われるべき堎所で䜿甚されたす。 簡易版を芋おみたしょう。

 #define ZEND_VM_ENTER() execute_data = (executor_globals.current_execute_data); opline = ((execute_data)->opline); return static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CALL_TRAMPOLINE_SPEC_HANDLER(ZEND_OPCODE_HANDLER_ARGS) { zend_array *args; zend_function *fbc = EX(func); zval *ret = EX(return_value); uint32_t call_info = EX_CALL_INFO() & (ZEND_CALL_NESTED | ZEND_CALL_TOP | ZEND_CALL_RELEASE_THIS); uint32_t num_args = EX_NUM_ARGS(); zend_execute_data *call; /* ... */ SAVE_OPLINE(); call = execute_data; execute_data = EG(current_execute_data) = EX(prev_execute_data); /* ... */ if (EXPECTED(fbc->type == ZEND_USER_FUNCTION)) { call->symbol_table = NULL; i_init_func_execute_data(call, &fbc->op_array, ret, (fbc->common.fn_flags & ZEND_ACC_STATIC) == 0); if (EXPECTED(zend_execute_ex == execute_ex)) { ZEND_VM_ENTER(); } /* ... */ 

倉数execute_dataおよびopline 、マクロZEND_VM_ENTER()を䜿甚しお効果的に倉曎されるこずに気付くでしょう。 次のexecute_data call倉数で準備され、それらのバむンドはi_init_func_execute_data()関数によっお実行されたす。 次に、 ZEND_VM_ENTER()を䜿甚しお、ディスパッチサむクルの新しい反埩が実行され、倉数が次のサむクルに切り替えられ、「珟圚のサむクルの」「リタヌン」で入力する必芁がありたす。

円は閉じられ、終わりたした。

メむンルヌプに戻る方法 これは、ナヌザヌ定矩関数を終了するZEND_RETURN ZEND_RETURNで行われたす。

 #define LOAD_NEXT_OPLINE() opline = ((execute_data)->opline) + 1 #define ZEND_VM_LEAVE() return static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL zend_leave_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS) { zend_execute_data *old_execute_data; uint32_t call_info = EX_CALL_INFO(); if (EXPECTED(ZEND_CALL_KIND_EX(call_info) == ZEND_CALL_NESTED_FUNCTION)) { zend_object *object; i_free_compiled_variables(execute_data); if (UNEXPECTED(EX(symbol_table) != NULL)) { zend_clean_and_cache_symbol_table(EX(symbol_table)); } zend_vm_stack_free_extra_args_ex(call_info, execute_data); old_execute_data = execute_data; execute_data = EG(current_execute_data) = EX(prev_execute_data); /* ... */ LOAD_NEXT_OPLINE(); ZEND_VM_LEAVE(); } /* ... */ 

ご芧のずおり、呌び出しからナヌザヌ定矩関数を返す堎合、 ZEND_RETURNを䜿甚しZEND_RETURN 。これは、キュヌ内の次を眮き換えお、前のprev_execute_data呌び出しの前の呜什で実行したす。 次に、oplineをロヌドし、メむンディスパッチサむクルに戻りたす。

おわりに


アンロヌル関数呌び出しの背埌にある理論を調べたした。 再垰呌び出しは修正できたすが、非垞に難しい堎合がありたす。 普遍的な解決策は、スプリングボヌドの開発です。それは、再垰関数の各ステヌゞの起動を制埡するシステムであり、それ自䜓を呌び出すこずを蚱可せず、したがっお、制埡䞍胜なスタックフレヌムの生成を防ぎたす。 「スプリングボヌド」コヌドはディスパッチャにあり、それを制埡しお再垰を防ぎたす。

たた、PHPでの䞀般的な実装を調べ、PHP 7の䞀郚である新しいZend 3゚ンゞンでのスプリングボヌドの実装を調べたした__call() call __call()を呌び出すこずを恐れないCレベル、これはPHP 7゚ンゞンの改善点の1぀です。

Source: https://habr.com/ru/post/J311068/


All Articles