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
And here is a semantically equivalent
if (isset ()) structure:
line
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
To understand the mechanism for calling a function, you need to know two things:
- function call and method call are the same
- a call to a user function and a call to an internal function are handled differently
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
Here you see another opcode - SEND_VAL. In total there are 4 types of opcode to send something to the function stack:
- SEND_VAL : sends constant value (string, integer, etc.)
- SEND_VAR : send PHP variable ($ a)
- SEND_REF : sends a PHP variable as a link to a function that takes an argument as a link.
- SEND_VAR_NO_REF : optimized handler used in cases with nested functions
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(); 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(); } 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) { zend_error(E_STRICT, "Non-static method %s::%s() should not be called statically", fbc->common.scope->name, fbc->common.function_name); } else { 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:
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); } else if (Z_TYPE_P(container) == IS_STRING && !prop_dim) { zval tmp; if (Z_TYPE_P(offset) == IS_LONG) { if (opline->extended_value & ZEND_ISSET) { if (offset->value.lval >= 0 && offset->value.lval < Z_STRLEN_P(container)) { result = 1; } } else { 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) { } 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) { } 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 ).