📜 ⬆️ ⬇️

Parsing function calls in PHP

This post is dedicated to optimizing PHP using a Blackfire profiler in a PHP script. The text below is a detailed technical explanation of a Blackfire blog article .

The strlen method is usually used:

if (strlen($name) > 49) { ... } 

However, this option is about 20% slower than this:
')
 if (isset($name[49])) { ... } 

Looks good. Surely you are ready to open your source code and replace all calls to strlen () with isset () . But if you carefully read the original article , you can see that the reason for the 20% difference in performance is multiple calls to strlen () , about 60-80 thousand iterations.

Why?


It's not about how strlen () calculates the lengths of strings in PHP, because all of them are already known by the time this method is called. Most, if possible, are computed at compile time. The length of a PHP string sent to memory is encapsulated in a C-structure containing this same string. Therefore, strlen () simply reads this information and returns as is. This is probably the fastest of the PHP functions, because it doesn't compute anything at all. Here is its source code:

 ZEND_FUNCTION(strlen) { char *s1; int s1_len; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &s1, &s1_len) == FAILURE) { return; } RETVAL_LONG(s1_len); } 

Considering that isset () is not a function, the reason for the 20% performance loss of strlen () is for the most part the attendant delays when calling a function in the Zend engine.

There is one more thing: when comparing the performance of strlen () with something else, an additional opcode is added. And in the case of isset () , only one unique opcode is used.

An example of a disassembled if (strlen ()) structure:

 line #* IO op fetch ext return operands ----------------------------------------------------------------------------------- 3 0 > SEND_VAR !0 1 DO_FCALL 1 $0 'strlen' 2 IS_SMALLER ~1 42, $0 3 > JMPZ ~1, ->5 5 4 > > JMP ->5 6 5 > > RETURN 1 

And here is a semantically equivalent if (isset ()) structure:

 line #* IO op fetch ext return operands ----------------------------------------------------------------------------------- 3 0 > ISSET_ISEMPTY_DIM_OBJ 33554432 ~0 !0, 42 1 > JMPZ ~0, ->3 5 2 > > JMP ->3 6 3 > > RETURN 1 

As you can see, the isset () code does not activate any function call (DO_FCALL). Also, there is no opcode IS_SMALLER (just ignore the RETURN statements); isset () directly returns a boolean value; strlen () first returns a temporary variable, then it is passed to opcode IS_SMALLER, and the final result is calculated using if () . That is, the strlen () structure uses two opcode, and the isset () structure uses one. Therefore, isset () demonstrates better performance, because one operation is usually performed faster than two.

Let's now figure out how function calls work in PHP and how they differ from isset () .

Function calls in php


The most difficult thing to analyze is that part of the virtual machine (the moment the PHP code is executed) that is associated with the function calls. I will try to state the essence, without going deep into the moments concerning function calls.

First, let's analyze the runtime (runtime) calls. Compile time requires a lot of resources to perform operations related to PHP functions. But if you use the opcode cache, then at compile time you will have no problems.

Suppose we have compiled a script. Let's analyze only what happens at run time . Here is the dump of the opcode call to the inner function (in this case, strlen () ):

 strlen($a); line #* IO op fetch ext return operands ----------------------------------------------------------------------------------- 3 0 > SEND_VAR !0 1 DO_FCALL 1 'strlen' 

To understand the mechanism for calling a function, you need to know two things:


That's why the last example talks about calling an “internal” function: strlen () is a PHP function that is part of the C code. If we had dumped the opcode “user-defined” PHP function (that is, a function that is written in PHP), we could get either the exact same or some other opcode.

The fact is that regardless of whether PHP knows this function or not, it does not generate the same opcode at compile time. Obviously, internal PHP functions are known at compile time, since they are declared before the compiler runs. But there can be no clarity regarding user-defined functions, because they can be called before they are declared. If we talk about execution, then internal PHP functions are more efficient than user ones, and moreover they have more validation mechanisms available.

From the example above, it can be seen that more than one opcode is used to control function calls. You also need to remember that functions have their own stack. In PHP, as in any other language, to call a function, you first need to create a stack frame and pass function arguments to it. Then you call the function that pulls these arguments from the stack for your needs. Upon completion of the call you have to destroy the frame created earlier.

So in general, the scheme of working with function calls looks like. However, PHP provides for optimizing the procedures for creating and deleting a stack frame; in addition, you can postpone their execution so that you do not have to do all these movements with each call of the function.

Opcode SEND_VAR is responsible for sending arguments to the stack frame. The compiler necessarily generates such an opcode before calling the function. And for each variable it creates its own:

 $a = '/'; setcookie('foo', 'bar', 128, $a); line #* IO op fetch ext return operands ----------------------------------------------------------------------------------- 3 0 > ASSIGN !0, '%2F' 4 1 SEND_VAL 'foo' 2 SEND_VAL 'bar' 3 SEND_VAL 128 4 SEND_VAR !0 5 DO_FCALL 4 'setcookie' 

Here you see another opcode - SEND_VAL. In total there are 4 types of opcode to send something to the function stack:



What does SEND_VAR do?

 ZEND_VM_HELPER(zend_send_by_var_helper, VAR|CV, ANY) { USE_OPLINE zval *varptr; zend_free_op free_op1; varptr = GET_OP1_ZVAL_PTR(BP_VAR_R); if (varptr == &EG(uninitialized_zval)) { ALLOC_ZVAL(varptr); INIT_ZVAL(*varptr); Z_SET_REFCOUNT_P(varptr, 0); } else if (PZVAL_IS_REF(varptr)) { zval *original_var = varptr; ALLOC_ZVAL(varptr); ZVAL_COPY_VALUE(varptr, original_var); Z_UNSET_ISREF_P(varptr); Z_SET_REFCOUNT_P(varptr, 0); zval_copy_ctor(varptr); } Z_ADDREF_P(varptr); zend_vm_stack_push(varptr TSRMLS_CC); FREE_OP1(); /* for string offsets */ CHECK_EXCEPTION(); ZEND_VM_NEXT_OPCODE(); } 

SEND_VAR checks if a variable is a link. If yes, then it separates it, thereby creating a disparity of the link. Why this is very bad, you can read in my other article . Then SEND_VAR adds the number of links to it (the link here is not a link in terms of PHP, that is, not that &, but just an indicator of how many people use this value) to the variable and sends it to the virtual machine stack:

 Z_ADDREF_P(varptr); zend_vm_stack_push(varptr TSRMLS_CC); 

Each time you call a function, you increment by one the refcount of each variable argument in the stack. This is because the variable will not be referenced by the function code, but by its stack. Sending a variable to the stack has little effect on performance, but the stack takes up memory. It is placed in it at runtime, but its size is calculated at compile time. After we send the variable to the stack, run DO_FCALL. Below is an example of how much code and checks are used only so that we consider calls to PHP functions to be "slow" statements (slow statement):

 ZEND_VM_HANDLER(60, ZEND_DO_FCALL, CONST, ANY) { USE_OPLINE zend_free_op free_op1; zval *fname = GET_OP1_ZVAL_PTR(BP_VAR_R); call_slot *call = EX(call_slots) + opline->op2.num; if (CACHED_PTR(opline->op1.literal->cache_slot)) { EX(function_state).function = CACHED_PTR(opline->op1.literal->cache_slot); } else if (UNEXPECTED(zend_hash_quick_find(EG(function_table), Z_STRVAL_P(fname), Z_STRLEN_P(fname)+1, Z_HASH_P(fname), (void **) &EX(function_state).function)==FAILURE)) { SAVE_OPLINE(); zend_error_noreturn(E_ERROR, "Call to undefined function %s()", fname->value.str.val); } else { CACHE_PTR(opline->op1.literal->cache_slot, EX(function_state).function); } call->fbc = EX(function_state).function; call->object = NULL; call->called_scope = NULL; call->is_ctor_call = 0; EX(call) = call; FREE_OP1(); ZEND_VM_DISPATCH_TO_HELPER(zend_do_fcall_common_helper); } 

As you can see, small checks are carried out here and various caches are used. For example, the handler pointer found the very first call, and then was cached into the main frame of the virtual machine so that each subsequent call could use this pointer.

Next we call zend_do_fcall_common_helper () . I will not post here the code of this function, it is too voluminous. I will show only those operations that were performed there. In short, this is a variety of different checks made during execution. PHP is a dynamic language, at runtime it can declare new functions and classes, simultaneously downloading files automatically. Therefore, PHP is forced to perform many checks at runtime, which has a bad effect on performance. But this is not going anywhere.

 if (UNEXPECTED((fbc->common.fn_flags & (ZEND_ACC_ABSTRACT|ZEND_ACC_DEPRECATED)) != 0)) { if (UNEXPECTED((fbc->common.fn_flags & ZEND_ACC_ABSTRACT) != 0)) { zend_error_noreturn(E_ERROR, "Cannot call abstract method %s::%s()", fbc->common.scope->name, fbc->common.function_name); CHECK_EXCEPTION(); ZEND_VM_NEXT_OPCODE(); /* Never reached */ } if (UNEXPECTED((fbc->common.fn_flags & ZEND_ACC_DEPRECATED) != 0)) { zend_error(E_DEPRECATED, "Function %s%s%s() is deprecated", fbc->common.scope ? fbc->common.scope->name : "", fbc->common.scope ? "::" : "", fbc->common.function_name); } } if (fbc->common.scope && !(fbc->common.fn_flags & ZEND_ACC_STATIC) && !EX(object)) { if (fbc->common.fn_flags & ZEND_ACC_ALLOW_STATIC) { /* FIXME: output identifiers properly */ zend_error(E_STRICT, "Non-static method %s::%s() should not be called statically", fbc->common.scope->name, fbc->common.function_name); } else { /* FIXME: output identifiers properly */ /* An internal function assumes $this is present and won't check that. So PHP would crash by allowing the call. */ zend_error_noreturn(E_ERROR, "Non-static method %s::%s() cannot be called statically", fbc->common.scope->name, fbc->common.function_name); } } 

See how many checks? Go ahead:

 if (fbc->type == ZEND_USER_FUNCTION || fbc->common.scope) { should_change_scope = 1; EX(current_this) = EG(This); EX(current_scope) = EG(scope); EX(current_called_scope) = EG(called_scope); EG(This) = EX(object); EG(scope) = (fbc->type == ZEND_USER_FUNCTION || !EX(object)) ? fbc->common.scope : NULL; EG(called_scope) = EX(call)->called_scope; } 

You know that each function body has its own variable scope. The engine switches visibility tables before calling the function code, so if it requests a variable, it will be found in the corresponding table. And since the functions and methods are essentially the same, you can read about how to bind the $ this pointer to the method.

 if (fbc->type == ZEND_INTERNAL_FUNCTION) { 

As I said above, internal functions (which are part of C) have a different execution path, not the same as user functions. It is usually shorter and better optimized, because we can tell the engine information about internal functions, which is not the case with user-defined ones.

 fbc->internal_function.handler(opline->extended_value, ret->var.ptr, (fbc->common.fn_flags & ZEND_ACC_RETURN_REFERENCE) ? &ret->var.ptr : NULL, EX(object), RETURN_VALUE_USED(opline) TSRMLS_CC); 

The line above calls the internal function handler. In the case of our example regarding strlen (), this line will call the code:

 /* PHP's strlen() source code */ ZEND_FUNCTION(strlen) { char *s1; int s1_len; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &s1, &s1_len) == FAILURE) { return; } RETVAL_LONG(s1_len); } 

What does strlen () do? It retrieves the argument from the stack using zend_parse_parameters () . This is a “slow” function, because it has to raise the stack and convert the argument to the type expected by the function (in our case, to string). Therefore, no matter what you pass to the stack for strlen () , it may need to convert the argument, and this is not the easiest process in terms of performance. The source code zend_parse_parameters () gives a good idea of ​​how many operations the processor has to perform during the extraction of arguments from the stack frame of the function.

Go to the next step. We have just executed the code of the function body, now we need to “clean up”. Let's start with the restoration of the scope:

 if (should_change_scope) { if (EG(This)) { if (UNEXPECTED(EG(exception) != NULL) && EX(call)->is_ctor_call) { if (EX(call)->is_ctor_result_used) { Z_DELREF_P(EG(This)); } if (Z_REFCOUNT_P(EG(This)) == 1) { zend_object_store_ctor_failed(EG(This) TSRMLS_CC); } } zval_ptr_dtor(&EG(This)); } EG(This) = EX(current_this); EG(scope) = EX(current_scope); EG(called_scope) = EX(current_called_scope); } 

Then clear the stack:

 zend_vm_stack_clear_multiple(1 TSRMLS_CC); 

And finally, if any exceptions were made during the execution of the function, you need to send the virtual machine to the block to catch this exception:

 if (UNEXPECTED(EG(exception) != NULL)) { zend_throw_exception_internal(NULL TSRMLS_CC); if (RETURN_VALUE_USED(opline) && EX_T(opline->result.var).var.ptr) { zval_ptr_dtor(&EX_T(opline->result.var).var.ptr); } HANDLE_EXCEPTION(); } 

About PHP function calls


Now you can imagine how much time your computer spends on calling the “very small and simple” function strlen () . And since it is called repeatedly, increase this time, say, 25,000 times. This is how micro and milliseconds turn into full seconds ... Please note that I demonstrated only the most important instructions for us during each call to a PHP function. After that, a lot of interesting things happen. Also keep in mind that in the case of strlen () , only one line performs “useful work”, and the accompanying procedures for preparing a function call are larger in volume than the “useful” part of the code. However, in most cases, the own function code still affects performance more than the “auxiliary” engine code.

That part of the PHP code that refers to the function call in PHP 7 has been reworked to improve performance. However, this is not the end, and the PHP source code will be optimized more than once with each new release. Older versions were not forgotten, function calls were optimized in versions from 5.3 to 5.5. For example, in versions from 5.4 to 5.5, the method of calculating and creating a stack frame was changed (while maintaining compatibility). For the sake of interest, you can compare the changes in the execution module and the method of calling functions made in version 5.5 compared to 5.4.

I want to emphasize: all of the above does not mean that PHP is bad. This language has been developing for 20 years, a lot of very talented programmers worked on its source code. During this period, it has been reworked, optimized and improved many times. Proof of this is the fact that you are using PHP today and it demonstrates good overall performance in a wide variety of projects.

What about isset ()?


This is not a function, parentheses do not necessarily mean “function call”. isset () is included in the special opcode of the Zend virtual machine (ISSET_ISEMPTY), which does not initialize the function call and is not subject to the associated delays. Since isset () can use parameters of several types, its code in the Zend virtual machine is quite long. But if you leave only the part related to the offset parameter, you get something like this:

 ZEND_VM_HELPER_EX(zend_isset_isempty_dim_prop_obj_handler, VAR|UNUSED|CV, CONST|TMP|VAR|CV, int prop_dim) { USE_OPLINE zend_free_op free_op1, free_op2; zval *container; zval **value = NULL; int result = 0; ulong hval; zval *offset; SAVE_OPLINE(); container = GET_OP1_OBJ_ZVAL_PTR(BP_VAR_IS); offset = GET_OP2_ZVAL_PTR(BP_VAR_R); /* ... code pruned ... */ } else if (Z_TYPE_P(container) == IS_STRING && !prop_dim) { /* string offsets */ zval tmp; /* ... code pruned ... */ if (Z_TYPE_P(offset) == IS_LONG) { /* we passed an integer as offset */ if (opline->extended_value & ZEND_ISSET) { if (offset->value.lval >= 0 && offset->value.lval < Z_STRLEN_P(container)) { result = 1; } } else /* if (opline->extended_value & ZEND_ISEMPTY) */ { if (offset->value.lval >= 0 && offset->value.lval < Z_STRLEN_P(container) && Z_STRVAL_P(container)[offset->value.lval] != '0') { result = 1; } } } FREE_OP2(); } else { FREE_OP2(); } Z_TYPE(EX_T(opline->result.var).tmp_var) = IS_BOOL; if (opline->extended_value & ZEND_ISSET) { Z_LVAL(EX_T(opline->result.var).tmp_var) = result; } else { Z_LVAL(EX_T(opline->result.var).tmp_var) = !result; } FREE_OP1_VAR_PTR(); CHECK_EXCEPTION(); ZEND_VM_NEXT_OPCODE(); } 

If you remove the numerous decision points ( if constructions), then the “main” computational algorithm can be expressed as:

 if (offset->value.lval >= 0 && offset->value.lval < Z_STRLEN_P(container)) 

If offset is greater than zero (you did not mean isset ($ a [-42]) ) and strictly less than the length of the string, the result will be taken as 1. Then the result of the operation will be a boolean TRUE. Do not worry about the length calculation, Z_STRLEN_P (container) does not calculate anything. Remember that PHP already knows the length of your string. Z_STRLEN_P (container) simply reads this value into memory, which consumes very little processor resources.

Now you understand why, from the point of view of using line offset, processing a call to the strlen () function requires MUCH more computational resources than isset () processing. The latter is essentially "easier." Do not be afraid of a large number of conditional if statements, this is not the hardest part of the C-code. In addition, they can be optimized using the C-compiler. The handler code isset () does not search in hash tables, does not perform complex checks, does not assign a pointer to one of the stack frames, in order to later retrieve it. The code is much easier than the general code of the function call, and much less often accesses memory (this is the most important point). And if you loop up the repeated execution of such a line, you can achieve a big performance improvement. Of course, the results of one iteration of strlen () and isset () will differ slightly - by about 5 ms. But if you spend 50,000 iterations ...

Also note that isset () and empty () have almost the same source code . In the case of a line offset, empty () will differ from isset () only by additional reading, if the first character of the line is not 0. Since the codes empty () and isset () do not differ much from each other, then empty () will show the same performance as isset () (considering that both are used with the same parameters).

How can OPCache help us?


In short - nothing.

OPCache optimizes code. This can be read in the presentation . It is often asked whether it is possible to add an optimization pass, in which strlen () switches to isset () . No, It is Immpossible.

OPCache optimization passes are performed in OPArray before it is placed in shared memory. This happens at compile time, not at run time. How do we know at compile time that the variable that is passed to strlen () is a string ? This is a known PHP problem, and it is partly solved by HHVM / Hack. If we wrote down our variables with strong typing in PHP, then during the compiler passes one could optimize a lot more things (like in a virtual machine). Since PHP is a dynamic language, almost nothing is known at compile time. OPCache can only optimize static things known by the time a compilation starts. For example, this:

 if (strlen("foo") > 8) { /* do domething */ } else { /* do something else */ } 

During compilation it is known that the length of the string “foo” is no more than 8, so you can throw out all the opcode if (), and leave only the part with the if construct with the else.

 if (strlen($a) > 8) { /* do domething */ } else { /* do something else */ } 

But what is $ a ? Does it even exist? Is it a string? By the time of the passage of the optimizer, we still can not answer all these questions - this is the task for the execution module of the virtual machine. At compile time, we process the abstract structure, and the type and amount of memory we need will be known at run time.

OPCache optimizes many things, but because of the very nature of PHP, it cannot optimize everything. , Java . , PHP . read-only:

 class Foo { public read-only $a = "foo"; } 

, . , $a . , , - , OPCache. , , OPCache , , .


, - . . Blackfire , , . , . , . , , , , .

: PHP , . PHP- — , , . . , , . , . PHP , foreach() , opcode. while for , .

, , , - .

 phpversion() => use the PHP_VERSION constant php_uname() => use the PHP_OS constant php_sapi_name() => use the PHP_SAPI constant time() => read $_SERVER['REQUEST_TIME'] session_id() => use the SID constant 

, .

, , :

 function foo() { bar(); } 

:

 function foo() { call_user_func_array('bar', func_get_args()); } 

, , - - - . , .

, . .

Blackfire , . ( GUI ): , , , / , , , foreach() .. etc.

, . , - . PHP ORM, , HTTP- . , . , : Java, Go , , — C/C++ (Java Go ).

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


All Articles