PHP中的SEPARATE_ARRAY

本篇文章基于PHP的commit为:9fa1d1330138ac424f990ff03e62721120aaaec3

PHP内核里面,有一个叫做SEPARATE_ARRAY的宏。长这样:

1
2
3
4
5
6
7
8
9
10
#define SEPARATE_ARRAY(zv) do {                     \
zval *_zv = (zv); \
zend_array *_arr = Z_ARR_P(_zv); \
if (UNEXPECTED(GC_REFCOUNT(_arr) > 1)) { \
if (Z_REFCOUNTED_P(_zv)) { \
GC_DELREF(_arr); \
} \
ZVAL_ARR(_zv, zend_array_dup(_arr)); \
} \
} while (0)

一句话来说,这个宏做的事情就是分离zend_array。我们知道,PHP是通过引用计数来管理多个变量对数组的引用,如果其中一个变量需要去修改数组的内容,那么底层就会单独为这个变量分配一个新的zend_array,并且原来的zend_array的引用计数减一。然后,SEPARATE_ARRAY做的事情就是这个。

因为PHP底层实在是有太多需要修改数组的操作了,所以我们确实需要SEPARATE_ARRAY来帮助我们去分离数组。

除此之外,我们会在zend_hash.c文件的所有写数组的函数里面发现HT_ASSERT_RC1这个断言宏。这个宏对于写C扩展的我们来说,在debug上是非常的有帮助的。我们来看看HT_ASSERT_RC1这个宏:

1
2
3
4
5
6
7
8
#define HT_ASSERT_RC1(ht) HT_ASSERT(ht, GC_REFCOUNT(ht) == 1)

#if ZEND_DEBUG
# define HT_ASSERT(ht, expr) \
ZEND_ASSERT((expr) || (HT_FLAGS(ht) & HASH_FLAG_ALLOW_COW_VIOLATION))
#else
# define HT_ASSERT(ht, expr)
#endif

首先,这个宏只会在PHP开启debug的时候才会起作用。

然后,我们发现,这个断言宏能够成功的情况有两个。一个是设置了zend_arrayHASH_FLAG_ALLOW_COW_VIOLATION标志;第二个是zend_array的引用计数是1。我们先来说一下第二点,因为第一点和第二点有关系。

为什么zend_array的引用计数要是1

因为PHP扩展操作数组的函数没法对数组进行分离。我们知道,如果修改一个数组,是需要发生写时复制的(我们也可以叫做写时分离),如果不进行写时复制,那么就会导致其他引用了这个数组的变量出问题(可能这是一个你意想不到的修改)。所以,只要这个数组的引用计数是1,我们就可以确定,这个数组只有一个变量在引用,所以我们可以放心的去修改它了。

那么,如果我们非要在引用计数大于1的时候去修改这个数组呢?那么第一种情况就起作用了,我们可以设置HASH_FLAG_ALLOW_COW_VIOLATION这个标志(翻译过来就是允许违反写时复制),来强制不遵守写时复制的规则。