Press "Enter" to skip to content

Understanding PHP 7 Internals: zval

PHP7 has been released. As promised, I'm going to start writing this series of articles. I mainly want, through these articles, to help you understand what we actually did behind PHP7's huge performance gains. Today I'd like to first chat with everyone about the changes to zval. Before talking about the zval changes, let's first look at what zval looked like in PHP5.

zval recap

In PHP5, the zval definition was as follows:

struct _zval_struct {
	union {
		long lval;
		double dval;
		struct {
			char *val;
			int len;
		} str;
		HashTable *ht;
		zend_object_value obj;
		zend_ast *ast;
	} value;
	zend_uint refcount__gc;
	zend_uchar type;
	zend_uchar is_ref__gc;
};

For folks who have some understanding of the PHP5 kernel, this struct should be quite familiar. Because zval can represent all data types in PHP, it includes a type field indicating what type of value this zval stores. Common possible options are IS_NULL, IS_LONG, IS_STRING, IS_ARRAY, IS_OBJECT, and so on.
Depending on the value of the type field, we have to interpret the value field in different ways. This value is a union. For example, if type is IS_STRING, we should use value.str to interpret the zval.value field; and if type is IS_LONG, we have to use value.lval to interpret it.
Additionally, we know PHP uses reference counting for basic garbage collection, so zval has a refcount__gc field indicating the reference count of this zval. But there's one thing to explain here: before 5.3, this field's name was still called refcount. After 5.3, when introducing the new garbage collection algorithm to deal with circular reference counting, the author added a large number of macros to operate on refcount. To make errors surface faster, it was renamed to refcount__gc, forcing everyone to use macros to operate on refcount.
Similarly, there's also is_ref, whose value indicates whether a type in PHP is a reference. Here we can see that being a reference is a flag bit.
This is the zval from the PHP5 era. In 2013, when we were doing the opcache JIT for PHP5, because the JIT performed poorly in real projects, we instead came to realize many of this struct's problems. And the PHPNG project began precisely with rewriting this struct.

Existing problems

The PHP5 zval definition was born with Zend Engine 2. As time went on, the limitations of the design at the time became more and more obvious:
First, this struct's size is (on a 64-bit system) 24 bytes. If we look carefully at this zval.value union, the longest plank is zend_object_value — it forces the whole value to need 16 bytes. This should be fairly easy to optimize away, e.g. move it out and replace it with a pointer, because IS_OBJECT isn't actually the most commonly used type either.
Second, every field of this struct has a clearly defined meaning, with no reserved custom fields at all. This meant that in the PHP5 era, when doing many optimizations that needed to store some zval-related information, we had no choice but to use other struct mappings, or an external-wrapper-then-patch approach to extend zval. For example, in 5.3 a GC specifically introduced to solve circular references had to adopt the following rather hacky approach:

/* The following macroses override macroses from zend_alloc.h */
#undef  ALLOC_ZVAL
#define ALLOC_ZVAL(z)                                   \
    do {                                                \
        (z) = (zval*)emalloc(sizeof(zval_gc_info));     \
        GC_ZVAL_INIT(z);                                \
    } while (0)

It hijacked zval allocation with zval_gc_info:

typedef struct _zval_gc_info {
    zval z;
    union {
        gc_root_buffer       *buffered;
        struct _zval_gc_info *next;
    } u;
} zval_gc_info;

Then it used zval_gc_info to extend zval. So in reality, allocating a zval in the PHP5 era actually really allocated 32 bytes. But GC really only needs to care about IS_ARRAY and IS_OBJECT types, which led to a lot of memory waste.
For example, the Taint extension I made earlier — I needed to store some marks for some strings, and there was nowhere in zval to use, so I had to adopt very unusual means:

Z_STRVAL_PP(ppzval) = erealloc(Z_STRVAL_PP(ppzval), Z_STRLEN_PP(ppzval) + 1 + PHP_TAINT_MAGIC_LENGTH);
PHP_TAINT_MARK(*ppzval, PHP_TAINT_MAGIC_POSSIBLE);

That is, extending the string's length by one int, then using a magic number as a mark written after it. This kind of approach has no guarantee of safety or stability from a technical standpoint.
Third, most of PHP's zvals are passed by value — copy-on-write values — but there are two exceptions, namely objects and resources, which are always passed by reference. This causes a problem: objects and resources, in addition to the reference counting in the zval, still need a global reference count, so that memory can be reclaimed. So in the PHP5 era, taking objects as an example, they had two sets of reference counts — one in the zval, and the other being obj's own count:

typedef struct _zend_object_store_bucket {
    zend_bool destructor_called;
    zend_bool valid;
    union _store_bucket {
        struct _store_object {
            void *object;
            zend_objects_store_dtor_t dtor;
            zend_objects_free_object_storage_t free_storage;
            zend_objects_store_clone_t clone;
            const zend_object_handlers *handlers;
            zend_uint refcount;
            gc_root_buffer *buffered;
        } obj;
        struct {
            int next;
        } free_list;
    } bucket;
} zend_object_store_bucket;

In addition to the two sets of references mentioned above, if we want to get an object, we need to do so via the following method:

EG(objects_store).object_buckets[Z_OBJ_HANDLE_P(z)].bucket.obj

Only after many long memory reads can we get to the real object itself. The efficiency can be imagined.
All of this is because when the Zend engine was first designed, it didn't consider the objects that came later. A good design, once it has an unexpected turn, will cause the whole structure to become complex and reduce maintainability. This is a very good example.
Fourth, we know that in PHP, a large amount of computation is string-oriented. However, because reference counting acts on the zval, this means that if we want to copy a string-type zval, we have no choice but to copy the string. When we add a zval's string as a key into an array, we have no choice but to copy the string. Although in PHP5.4 we introduced the INTERNED STRING, it still couldn't fundamentally solve this problem.
For example, in PHP a lot of structs are implemented based on the Hashtable. Adding/removing/changing/looking-up on the Hashtable takes up a lot of CPU time, and looking up a string first requires its Hash value. Theoretically we could completely compute a string's Hash value once, store it, and avoid recomputing, and so on.
Fifth, this is about references. In the PHP5 era, we used copy-on-write, but combined with references there's a classic performance problem:

<?php
    function dummy($array) {}
    $array = range(1, 100000);
    $b = &$array;
    dummy($array);
?>

When we call dummy, it's a place where a simple pass-by-value would do. But because $array was once reference-assigned to $b, $array became a reference, so a separation happens here, causing an array copy, which drastically slows down performance. Here's a simple test:

<?php
$array = range(1, 100000);
function dummy($array) {}
$i = 0;
$start = microtime(true);
while($i++ < 100) {
    dummy($array);
}
printf("Used %sS\n", microtime(true) - $start);
$b = &$array; //note here, suppose I accidentally reference this Array to a variable
$i = 0;
$start = microtime(true);
while($i++ < 100) {
    dummy($array);
}
printf("Used %ss\n", microtime(true) - $start);
?>

Running this example under 5.6, we get the following results:

$ php-5.6/sapi/cli/php /tmp/1.php
Used 0.00045204162597656s
Used 4.2051479816437s

A difference of as much as 10,000x. This means that if, in a big chunk of code, I accidentally turn a variable into a reference (e.g. foreach as &$v), it's possible to trigger this problem, causing a serious performance issue, yet it's hard to track down.
Sixth, and most importantly — why do I say it's important? Because this point led to a very large performance gain. We'd gotten used to calling MAKE_STD_ZVAL in the PHP5 era to allocate a zval on the heap, then operating on it, and finally copying this zval's value "to" return_value via RETURN_ZVAL, and then destroying this zval. For example, the pathinfo function:

PHP_FUNCTION(pathinfo)
{
.....
	MAKE_STD_ZVAL(tmp);
	array_init(tmp);
.....
    if (opt == PHP_PATHINFO_ALL) {
        RETURN_ZVAL(tmp, 0, 1);
    } else {
.....
}

This tmp variable is purely a temporary variable's role — why bother allocating it on the heap? MAKE_STD_ZVAL/ALLOC_ZVAL in PHP5 were everywhere, a very common usage. If we could allocate this variable on the stack, it would be very beneficial whether for memory allocation or cache-friendliness.
There are many more; I won't list them all in detail. But I believe you've had the same thought we had at the time: zval really needed to be changed, right?

The current zval

In PHP7, zval became the following struct. I should note that this is the current structure — it has already some differences from the PHPNG era, because we added some interpretations (union fields), but the overall size and structure are consistent with the PHPNG era:

struct _zval_struct {
	union {
		zend_long         lval;             /* long value */
		double            dval;             /* double value */
		zend_refcounted  *counted;
		zend_string      *str;
		zend_array       *arr;
		zend_object      *obj;
		zend_resource    *res;
		zend_reference   *ref;
		zend_ast_ref     *ast;
		zval             *zv;
		void             *ptr;
		zend_class_entry *ce;
		zend_function    *func;
		struct {
			uint32_t w1;
			uint32_t w2;
		} ww;
	} value;
    union {
        struct {
            ZEND_ENDIAN_LOHI_4(
                zend_uchar    type,         /* active type */
                zend_uchar    type_flags,
                zend_uchar    const_flags,
                zend_uchar    reserved)     /* call info for EX(This) */
        } v;
        uint32_t type_info;
    } u1;
    union {
        uint32_t     var_flags;
        uint32_t     next;                 /* hash collision chain */
        uint32_t     cache_slot;           /* literal cache slot */
        uint32_t     lineno;               /* line number (for ast nodes) */
        uint32_t     num_args;             /* arguments number for EX(This) */
        uint32_t     fe_pos;               /* foreach position */
        uint32_t     fe_iter_idx;          /* foreach iterator index */
    } u2;
};

Although it looks very big, if you look carefully, it's all unions. This new zval in a 64-bit environment now only needs 16 bytes (2 pointer sizes). It's mainly divided into two parts: value and the extension fields. The extension fields are further divided into two parts, u1 and u2, where u1 is the type info, and u2 is various auxiliary fields.

The value part is size_t sized (one pointer size); it can store a pointer, or a long, or a double.
The type info part stores this zval's type. The extension auxiliary fields are used in many other places; for example, next is used to replace the original chain pointer in the Hashtable. This part will be explained in detail later when I introduce the HashTable.

Types

The types of zval in PHP7 underwent fairly major adjustments. Overall there are the following 17 types:

/* regular data types */
#define IS_UNDEF                    0
#define IS_NULL                     1
#define IS_FALSE                    2
#define IS_TRUE                     3
#define IS_LONG                     4
#define IS_DOUBLE                   5
#define IS_STRING                   6
#define IS_ARRAY                    7
#define IS_OBJECT                   8
#define IS_RESOURCE                 9
#define IS_REFERENCE                10
/* constant expressions */
#define IS_CONSTANT                 11
#define IS_CONSTANT_AST             12
/* fake types */
#define _IS_BOOL                    13
#define IS_CALLABLE                 14
/* internal types */
#define IS_INDIRECT                 15
#define IS_PTR                      17

Among these, the IS_BOOL type from the PHP5 era is now split into two types, IS_FALSE and IS_TRUE. And the original reference was a flag bit; now the reference is a new type.
For IS_INDIRECT and IS_PTR, these two types are internal reserved types; users won't perceive them. This part will also be introduced together when I introduce the HashTable later.
From PHP7 onward, for values that can fit in the zval's value field, we no longer do reference counting on them — instead we directly assign them at copy time. This saves a large amount of reference-counting-related operations. These types are:

IS_LONG
IS_DOUBLE

For those types that have no value at all, only a type, no reference counting is needed either:

IS_NULL
IS_FALSE
IS_TRUE

And for complex types, those that can't be stored in a single size_t, we use value to store a pointer; this pointer points to the concrete value, and reference counting acts on that value accordingly, rather than on the zval.
PHP7 zval diagram
Taking IS_ARRAY as an example:

struct _zend_array {
    zend_refcounted_h gc;
    union {
        struct {
            ZEND_ENDIAN_LOHI_4(
                zend_uchar    flags,
                zend_uchar    nApplyCount,
                zend_uchar    nIteratorsCount,
                zend_uchar    reserve)
        } v;
        uint32_t flags;
    } u;
    uint32_t          nTableMask;
    Bucket           *arData;
    uint32_t          nNumUsed;
    uint32_t          nNumOfElements;
    uint32_t          nTableSize;
    uint32_t          nInternalPointer;
    zend_long         nNextFreeElement;
    dtor_func_t       pDestructor;
};

zval.value.arr points to a struct like the one above, which actually stores an array. The reference-counting part is stored in the zend_refcounted_h struct:

typedef struct _zend_refcounted_h {
    uint32_t         refcount;          /* reference counter 32-bit */
    union {
        struct {
            ZEND_ENDIAN_LOHI_3(
                zend_uchar    type,
                zend_uchar    flags,    /* used for strings & objects */
                uint16_t      gc_info)  /* keeps GC root number (or 0) and color */
        } v;
        uint32_t type_info;
    } u;
} zend_refcounted_h;

All complex type definitions begin with the zend_refcounted_h struct. This struct, besides the reference count, also has GC-related structures. Thus when doing GC reclamation, GC doesn't need to care what the concrete type is — all of them can be treated as a zend_refcounted* struct.
Another thing to note is the ZEND_ENDIAN_LOHI_4 macro, which some of you might be curious about. The role of this macro is to simplify assignment: it guarantees that on big-endian or little-endian machines, the fields it defines are all stored in the same order, so that when we assign, we don't need to assign its fields separately, but can assign them uniformly. For example, taking the array struct above, we can do it via:

arr1.u.flags = arr2.u.flags;

To complete in one step what is equivalent to the following assignment sequence:

arr1.u.v.flags				= arr2.u.v.flags;
arr1.u.v.nApplyCount 		= arr2.u.v.nApplyCount;
arr1.u.v.nIteratorsCount	= arr2.u.v.nIteratorsCount;
arr1.u.v.reserve 			= arr2.u.v.reserve;

There's also a question some of you might ask: why not put the type field in front of the zval's type, because we know that when we use a zval, the first thing is definitely to get its type first. One reason here is that the difference between the two is not big; another is considering that if we do JIT in the future, if the zval's type could be obtained through type inference, there'd be no need at all to read its type value.

Flag bits

Besides the data type, past experience also tells us that a data, besides its type, should also have many other attributes. For example, INTERNED STRING is a string that exists throughout the whole PHP request (e.g. a literal you write in code); it isn't reclaimed by reference counting. In the 5.4 version we did this by pre-allocating a block of memory, then allocating the string in this memory, and finally comparing pointer addresses — if a string was within the memory range of INTERNED STRING, we considered it an INTERNED STRING. The downside of doing this is obvious: when there's not enough memory, we have no way to allocate an INTERNED STRING. It's also very ugly. So if a string could have some attribute definitions, this implementation would become very elegant.
Also, for example, now we no longer do reference counting for types like IS_LONG, IS_TRUE. So when we get a zval, how do we determine whether it needs reference counting? Naturally we might say to use:

if (Z_TYPE_P(zv) >= IS_STRING) {
  //needs reference counting
}

But you forgot, there's also the existence of INTERNED STRING, so you'd maybe have to write it like this:

if (Z_TYPE_P(zv) >= IS_STRING && !IS_INTERNED(Z_STR_P(zv))) {
  //needs reference counting
}

Doesn't it already start to feel a bit off? Hmm, don't rush, there's more. We also introduced constant arrays in 5.6. This array is stored in Opcache's shared memory, and it also doesn't need reference counting:

if (Z_TYPE_P(zv) >= IS_STRING && !IS_INTERNED(Z_STR_P(zv))
    && (Z_TYPE_P(zv) != IS_ARRAY || !Z_IS_IMMUTABLE(Z_ARRVAL(zv)))) {
 //needs reference counting
}

Don't you also think this is just too ugly — you just can't stand such verbose code, right?
Yes, we thought of this long ago. Looking back at the previous zval definition, did you notice type_flags? We introduced a flag bit called IS_TYPE_REFCOUNTED, which is stored in zval.u1.v.type_flags. We assign this flag to the types that need reference counting, so the judgment above can become very elegant:

if (!(Z_TYPE_FLAGS(zv) & IS_TYPE_REFCOUNTED)) {
}

And for INTERNED STRING, this IS_STR_INTERNED flag bit should act on the string itself rather than the zval.
So how many such flag bits are there in total? Those acting on the zval are:

IS_TYPE_CONSTANT            //is a constant type
IS_TYPE_IMMUTABLE           //immutable type, e.g. arrays stored in shared memory
IS_TYPE_REFCOUNTED          //type that needs reference counting
IS_TYPE_COLLECTABLE         //type that may contain circular references (IS_ARRAY, IS_OBJECT)
IS_TYPE_COPYABLE            //type that can be copied. Remember the exceptions I mentioned earlier for objects and resources? Objects and resources are not.
IS_TYPE_SYMBOLTABLE         //zval stores the global symbol table. This became useless after I made an adjustment earlier, but it's still kept for compatibility;
                            //it will be removed in the next version

Those acting on strings are:

IS_STR_PERSISTENT	        //is a string with malloc-allocated memory
IS_STR_INTERNED             //INTERNED STRING
IS_STR_PERMANENT            //immutable string, used as a sentinel
IS_STR_CONSTANT             //string representing a constant
IS_STR_CONSTANT_UNQUALIFIED //constant string that may carry a namespace

Those acting on arrays are:

#define IS_ARRAY_IMMUTABLE  //same as IS_TYPE_IMMUTABLE

Those acting on objects are:

IS_OBJ_APPLY_COUNT          //recursion protection
IS_OBJ_DESTRUCTOR_CALLED    //destructor already called
IS_OBJ_FREE_CALLED          //free function already called
IS_OBJ_USE_GUARDS           //magic method recursion protection
IS_OBJ_HAS_GUARDS           //whether there's a magic method recursion protection flag

With these reserved flag bits, we can conveniently do things that were hard to do before. For example, my own Taint extension — now marking a string as a tainted string becomes incredibly simple:

/* it's important that make sure
 * this value is not used by Zend or
 * any other extension agianst string */
#define IS_STR_TAINT_POSSIBLE    (1<<7)
#define TAINT_MARK(str)     (GC_FLAGS((str)) |= IS_STR_TAINT_POSSIBLE)

This mark will persist for the whole life of this string, saving me a lot of my earlier tricky approaches.

zval pre-allocation

Earlier we said PHP5's zval allocation used heap memory allocation, i.e. the MAKE_STD_ZVAL and ALLOC_ZVAL macros seen everywhere in PHP extension code. We also learned that a zval originally only needed 24 bytes, but counting gc_info, it actually allocated 32 bytes. Plus PHP's own memory management reserves some information in front of the memory when allocating:

typedef struct _zend_mm_block {
    zend_mm_block_info info;
#if ZEND_DEBUG
    unsigned int magic;
# ifdef ZTS
    THREAD_T thread_id;
# endif
    zend_mm_debug_info debug;
#elif ZEND_MM_HEAP_PROTECTION
    zend_mm_debug_info debug;
#endif
} zend_mm_block;

So as a result, we only needed 24 bytes of memory, but in the end it ended up allocating as many as 48 bytes.
However, for most zvals, especially the zvals inside extension functions, think about it: the parameters it receives come from external zvals, and it returns the return value to return_value, which is also an external zval, while the intermediate variable zvals can entirely be allocated on the stack. That is, most internal functions don't need to allocate memory on the heap — the zvals they need can all come from outside.
So at the time we had a bold idea: no zval needs to be individually allocated.
And this is easy to prove: the zvals used in a PHP script either exist in the symbol table, or exist in the form of a temporary variable (IS_TMP_VAR) or a compiled variable (IS_CV). The former exists in a Hashtable, and in PHP7 the HashTable stores zvals by default, so this set of zvals can be allocated all at once when the HashTable is allocated. The latter exists after execute_data, and its count is also determined at compile time, so it can also be allocated all at once with execute_data. So indeed we no longer need to allocate zvals individually on the heap.
So, starting from PHP7, we removed the MAKE_STD_ZVAL/ALLOC_ZVAL macros and no longer support allocating zvals on the heap. The zvals used inside a function either come from external input, or use temporary zvals allocated on the stack.
In later practice, the biggest change summarized for developers is: previously some internal functions, after getting some information through some operations, would allocate a zval and return it to the caller:

static zval * php_internal_function() {
    .....
    str = external_function();
    MAKE_STD_ZVAL(zv);
    ZVAL_STRING(zv, str, 0);
	return zv;
}
PHP_FUNCTION(test) {
	RETURN_ZVAL(php_internal_function(), 1, 1);
}

Either change it so that this zval is passed in by the caller:

static void php_internal_function(zval *zv) {
    .....
    str = external_function();
    ZVAL_STRING(zv, str);
	efree(str);
}
PHP_FUNCTION(test) {
	php_internal_function(return_value);
}

Either change it so that this function returns the raw material:

static char * php_internal_function() {
    .....
    str = external_function();
	return str;
}
PHP_FUNCTION(test) {
	str = php_internal_function();
	RETURN_STRING(str);
	efree(str);
}

Summary

(This part I haven't decided how to phrase it yet. Originally I wanted to lead into the fact that the HashTable no longer contains zval**, thereby leading into the necessity of the reference type's existence. But if we don't first talk about the HashTable's structure, this lead-in seems abrupt. Let's leave it like this for now, and I'll revise it later.)
By now we've basically finished introducing the overview of zval's changes. Abstractly speaking, actually in PHP7 the zval has already become a value pointer — it either stores the raw value, or stores a pointer pointing to a place that stores the raw value. That is, the current zval is equivalent to zval * in PHP5. Just that, compared to zval *, directly storing the zval lets us save one pointer dereference, thereby improving cache-friendliness.
In fact, for PHP7's performance, we didn't introduce any new technical paradigm. It mainly comes from the principles of continuously and tirelessly reducing memory usage, improving cache-friendliness, and reducing the number of executed instructions. You could say PHP7's refactoring is precisely these three principles.

Be First to Comment

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.