function factorial($n) { if ($n == 1) { return $n; } return $n * factorial($n-1); }
alloca()
).mov
, pop
, push
and call
) and always implies accessing main memory. And - it is slow. If your program can work with a single function, without calling children, it will act faster: the processor does not need to infinitely create and delete stacks, moving blocks of memory that the program does not use directly, they are simply part of the architecture. Today, processors usually use registers to store stack arguments or return addresses (for example, LP64 on Linux), but still avoid recursion, at least its deepest levels. function tail_factorial($n, $acc = 1) { if ($n == 1) { return $acc; } return tail_factorial($n-1, $n * $acc); }
function unrolled_factorial($n) { $acc = 1; while ($n > 1) { $acc *= $n--; } return $acc; }
factorial()
, but no longer calls itself. During runtime, this is much more productive than a recursive alternative.goto
branch: function goto_factorial($n) { $acc = 1; f: if ($n == 1) { return $acc; } $acc *= $n--; goto f; }
factorial()
with a huge number: you’re running out of a stack, and you’ll run into the engine’s memory limit (since the stack frames in the virtual machine are placed on the heap). If you disable the limit ( memory_limit ), then PHP will crash, because neither it nor the Zend virtual machine has protection against infinite recursion. Consequently, the process will collapse. Now try running with the same argument unrolled_factorial()
or even goto_factorial()
. The system will not fall. It may not run too fast, but it will not fall, and the place on the stack (located on the PHP heap) will not end. Although the speed of execution will be much higher than in the case of the recursive function.bsearch()
). 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);
is_callable()
call inside.opline
, and each iteration executes ( handler()
) one instruction ( opline
) of the virtual machine. A lot of things can happen within this instruction, but at the end there is always a command for the loop, usually a command for moving to the next iteration (goto next). There may also be a return command from an infinite loop or a transition command to this operation.execute_ex()
function. Here is an example for PHP 7 with some optimizations for my computer (IP and FP registers are used): #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)
structure. What about recursion? What's the matter?while(1)
as part of the execute_ex()
function. What happens if a single instruction ( opline->handler()
) starts execute_ex()
? Recursion will occur. This is bad. As usual: yes, if it is multi-level.execute_ex()
call execute_ex()
? Here I will not go too deep into the virtual machine engine, because you can miss a lot of important information. For simplicity, we assume that this PHP function call calls execute_ex()
.execute_ex()
call with new instructions for execution. When this loop appears, the PHP function call is completed, which leads to the return procedure in the code. Consequently, the current loop of the current frame on the stack ends with the return of the previous one. Just keep in mind that this happens only in the case of PHP functions in user space. The reason is that user-defined PHP functions are opcodes that, after starting, run in a loop. But internal PHP functions (developed in C and located in the kernel or in extensions) do not need to execute opcodes. These are instructions on pure C, therefore, they do not create another dispatch cycle and another frame.__call()
. This is a PHP function from user space. As with any user-defined function, its execution leads to a new execute_ex()
call. But the fact is that __call()
can be called multiple times, creating many frames. Every time an unknown method is called in the context of an object using __call()
defined in its class.__call()
, as well as using prevention of recursive calls to execute_ex()
in the case of __call()
.__call()
in PHP 5.6:execute_ex()
. This is taken from a PHP script that calls an unknown method in the context of an object, which in turn calls an unknown method in the context of another object (in both cases, the classes contain __call()
). So the first execute_ex()
is the execution of the main script (position 6 in the call stack), and at the top of the list we see the other two execute_ex()
.execute_ex()
, that is, one dispatch cycle that controls all the instructions, including __call()
calls.execute_ex()
in the context of __call()
. That is, we have prepared a new dispatch loop to execute the currently requested __call()
opcodes.fooBarDontExist()
be executed. We need to put in memory a number of structures and perform a classic function call from user space. Something like this (simplified): 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()
for the sake of better performance” (and for a number of other reasons). It really is.execute_ex()
. To do this, we derecurs the procedure, remaining in the same context as execute_ex()
, and also redirecting (rebranch) it to its beginning, changing the necessary arguments. Let's look again at 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
and execute_data
variables (contains the following opcode, and opline is the “current” opcode to execute). When we meet __call()
, then:opline
and execute_data
.orig_opline
and orig_execute_data
; the virtual machine manager must always remember where it came from so that it can go (the branch) to wherever it goes).ZEND_CALL_TRAMPOLINE
. It is used wherever __call()
calls should be made. Let's look at the simplified version: #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
and opline
effectively modified using the macro ZEND_VM_ENTER()
. The following execute_data
prepared in the call
variable, and their binding (bind) is performed by the i_init_func_execute_data()
function. Next, using ZEND_VM_ENTER()
, a new iteration of the dispatching cycle is performed, which switches the variables to the next cycle, and they must go into it with a “return” (the current cycle).ZEND_RETURN
, which completes any user-defined function. #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
, which replaces the next ones in the queue to execute the instructions from the previous ones from the previous call, prev_execute_data
. Then it loads the opline and returns to the main dispatch loop.__call()
call __call()
, they work faster than in PHP 5. They do not create new frame stacks (for C-level), this is one of the improvements in the PHP 7 engine.Source: https://habr.com/ru/post/311068/
All Articles