- URL: https://www.laruence.com/en/2020/02/27/5213.html
- Please include attribution when republishing.
Starting from PHP7, you may have noticed that many functions no longer use the traditional parameter-handling approach, but instead switched to a newer method we call Fast zend parameters parsing (FAST_ZPP). For example, before PHP7, the count function looked like this:
PHP_FUNCTION(count)
{
zval *array;
long mode = COUNT_NORMAL;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z|l", &array, &mode) == FAILURE) {
return;
}
....
}
After PHP7, it became:
PHP_FUNCTION(count)
{
zval *array;
zend_long mode = COUNT_NORMAL;
ZEND_PARSE_PARAMETERS_START(1, 2)
Z_PARAM_ZVAL(array)
Z_PARAM_OPTIONAL
Z_PARAM_LONG(mode)
ZEND_PARSE_PARAMETERS_END();
...
}
Many PHP extension developers probably found it quite unfamiliar when they first encountered it. Don't panic — let me walk through it slowly. 🙂
Back when we were developing PHPNG (the codename for the PHP7 project), one of our main ways of finding performance improvements was to benchmark various large real-world projects, to locate the parts that consume the most resources. One of the most commonly used benchmark targets is wordpress, because it's complex enough and slow enough (it's also a primary benchmark target when we were developing the JIT :)). It represents a typical application of non-OO-style code. In the course of actual benchmarking, we found that nearly 6% of the time was being consumed by zend_parse_parameters.
In fact, zend_parameters_parsing is indeed a very large function:
ZEND_API int zend_parse_parameters(int num_args, const char *type_spec, ...)
It processes the input parameters according to the identifiers specified in the type_spec string, and there are many of these parameter characters (for their specific meanings, see: README.PARAMETER_PARSING_API):
a A b C d f h H l L o O p P r s S z * + | / !
Different combinations represent the types of parameters our PHP function is to accept. For example, in the count example, "z|l" means it accepts one zval-type argument and one optional long-type mode argument. When zend_parse_parameters is called at runtime, it needs to parse these characters and then invoke the corresponding logic. For functions that are themselves quite simple, like count, this overhead becomes quite noticeable.
Looking back at the characteristics of this function, we find that, for the count example, the type_spec is actually a constant determined at compile time. That is, at compile time, we should already know what the corresponding argument-handling logic for "a|l" ought to be.
In fact, modern compilers all have this basic optimization capability. For example, for the following code:
#include <stdlib.h>
#define AAA 1;
int main() {
int a = AAA;
if (a) {
abort();
}
return 0;
}
If we try to compile-optimize it (-O2) and inspect the generated assembly:
main:
.LFB18:
subq $8, %rsp
call abort@PLT
As you can see, the if check has been eliminated, because at compile time it's known that a is 1, so the if is always true.
FAST_ZPP is a new kind of parameter-declaration approach that makes full use of this capability. For example, for Z_PARAM_ZVAL(array)
#define Z_PARAM_ZVAL_EX(dest, check_null, separate) \
if (separate) { \
Z_PARAM_PROLOGUE(separate); \
zend_parse_arg_zval_deref(_arg, &dest, check_null); \
} else { \
++_i; \
ZEND_ASSERT(_i <= _min_num_args || _optional==1); \
ZEND_ASSERT(_i > _min_num_args || _optional==0); \
if (_optional && UNEXPECTED(_i >_num_args)) break; \
_real_arg++; \
zend_parse_arg_zval(_real_arg, &dest, check_null); \
}
#define Z_PARAM_ZVAL(dest) \
Z_PARAM_ZVAL_EX(dest, 0, 0)
At compile time, it can first be replaced with:
zend_parse_arg_zval(((zval*)execute_data) - 1, &array, 0);
And if we look further at zend_parse_arg_zval:
static zend_always_inline void zend_parse_arg_zval(zval *arg, zval **dest, int check_null)
{
*dest = (check_null &&
(UNEXPECTED(Z_TYPE_P(arg) == IS_NULL) ||
(UNEXPECTED(Z_ISREF_P(arg)) &&
UNEXPECTED(Z_TYPE_P(Z_REFVAL_P(arg)) == IS_NULL)))) ? NULL : arg;
}
We find that it too is an inline-declared function, and since the arguments are constants, it can be further evaluated down to:
zval *array = ((zval*)execute_data) - 1;
So, how's that — doesn't it look like it will be a lot faster at a glance? No type_spec parsing, no extra function call, just directly fetches the argument.
The inline function mentioned earlier — being able to prune and inline at compile time based on constants — is also a very good method for avoiding duplicated code across similar functions, and it's used extensively in PHP7. Those interested can look at the definitions of many similar functions in zend_hash.c.
Of course, there's a downside to doing this: it increases our program's binary size. This is easy to understand — for example, for count, originally it just called one external function, so a single call instruction was enough, but now there will be many inlined instructions.
And once the binary size grows, cache misses at execution time increase, which also affects performance. So we don't recommend using FAST_ZPP for everything; rather, we recommend using it for functions that are called frequently in actual applications and whose own logic is relatively simple.
To sum up: in general, the extension functions we write ourselves don't necessarily need to use FAST_ZPP, because if the function logic is complex, this overhead is relatively acceptable by comparison.
Finally, here's the correspondence between the new FAST_ZPP API and the old parameter descriptors:

Be First to Comment