debug_zval_dump() 无法输出int等类型的引用计数?
一起读源码
xdebug_debug_zval与它的区别
环境变量:
MacOS High Sierra
PHP-7.2.17-debug
GDB-8.0.1
Q debug_zval_dump()
该函数可以打印变量的相关信息,记录变量被引用的次数并支持不定长参数。但是我在使用过程中发现对于不可变变量,例如FALSE、TRUE、INT、DOUBLE、NULL这些类型的变量。虽然被引用后增加了引用计数,但是该函数不会打印出来。运行结果如下:
1 2 3 4 5 6 7 8 9 10 <?php $a = 1 ; $b = &$a; debug_zval_dump($a); xdebug_debug_zval("a" );
以下是我使用GDB调试时的观察,执行完下面一段代码发生的变化有:
所以xdebug输出的信息符合预期。那在PHP7中,自带的这个debug函数,为什么不能输出我们预期的结果,它又是怎么实现该方法的呢?
一起读源码 主函数部分 下面这部分代码是主函数。第一块是变量的定义;
第二块宏是解析可变参数的,将传来的一个或多个参数放入args;
第三块for是调用函数php_debug_zval_dump
对参数args进行循环打印。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 PHP_FUNCTION(debug_zval_dump) { zval *args; int argc; int i; ZEND_PARSE_PARAMETERS_START(1 , -1 ) Z_PARAM_VARIADIC('+' , args, argc) ZEND_PARSE_PARAMETERS_END(); for (i = 0 ; i < argc; i++) { php_debug_zval_dump(&args[i], 1 ); } }
php_debug_zval_dump 紧接着我们来阅读这个函数的代码,可以看到。在初始化完变量后,进入到一个对level
的判断。不要迷惑,这是针对多级结构的空格 输出,从而使输出格式规范。
然后进入again块,根据zval中的type去判断类型从而走不同的逻辑。
一眼就能看出从case IS_FALSE
到 case IS_DOUBLE
都是直接输出,%s 对应 COMMON这个局部常量,判断is_ref
是否为1。如果为1,则在前方输出一个&
取地址符。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 PHPAPI void php_debug_zval_dump (zval *struc, int level) { HashTable *myht = NULL ; zend_string *class_name; int is_ref = 0 ; zend_ulong index; zend_string *key; zval *val; uint32_t count; if (level > 1 ) { php_printf("%*c" , level - 1 , ' ' ); } again: switch (Z_TYPE_P(struc)) { case IS_FALSE: php_printf("%sbool(false)\n" , COMMON); break ; case IS_TRUE: php_printf("%sbool(true)\n" , COMMON); break ; case IS_NULL: php_printf("%sNULL\n" , COMMON); break ; case IS_LONG: php_printf("%sint(" ZEND_LONG_FMT ")\n" , COMMON, Z_LVAL_P(struc)); break ; case IS_DOUBLE: php_printf("%sfloat(%.*G)\n" , COMMON, (int ) EG(precision), Z_DVAL_P(struc)); break ;
STRING case IS_STRING
这块,第一行输出固定格式,意义同上。字符串值的输出在第二行:PHPWRITE
,以流形式输出字符串,这里需要传递两个参数,一个是value的指针,一个是value的长度指针。第三行接着打印出它的引用计数。三元运算符判断如果zend_string中不存在,则默认为1。
1 2 3 4 5 case IS_STRING: php_printf("%sstring(%zd) \"" , COMMON, Z_STRLEN_P(struc)); PHPWRITE(Z_STRVAL_P(struc), Z_STRLEN_P(struc)); php_printf("\" refcount(%u)\n" , Z_REFCOUNTED_P(struc) ? Z_REFCOUNT_P(struc) : 1 ); break ;
ARRAY case IS_ARRAY
这块代码一开始就进行一个level判断,但主函数进行调用时,level的值为1,所以第一次不会进入此处。然后计算array的长度,按格式输出array的长度和引用计数,例如:array(3) refcount(2)
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 case IS_ARRAY: myht = Z_ARRVAL_P(struc); if (level > 1 && !(GC_FLAGS(myht) & GC_IMMUTABLE)) { if (GC_IS_RECURSIVE(myht)) { PUTS("*RECURSION*\n" ); return ; } GC_PROTECT_RECURSION(myht); } count = zend_array_count(myht); php_printf("%sarray(%d) refcount(%u){\n" , COMMON, count, Z_REFCOUNTED_P(struc) ? Z_REFCOUNT_P(struc) : 1 ); ZEND_HASH_FOREACH_KEY_VAL_IND(myht, index, key, val) { zval_array_element_dump(val, index, key, level); } ZEND_HASH_FOREACH_END(); if (level > 1 && !(GC_FLAGS(myht) & GC_IMMUTABLE)) { GC_UNPROTECT_RECURSION(myht); } if (level > 1 ) { php_printf("%*c" , level - 1 , ' ' ); } PUTS("}\n" ); break ;
程序继续执行,到了宏定义ZEND_HASH_FOREACH_KEY_VAL_IND
此处,循环hash数组,输出数组内的元素。但注意在宏内调用了zval_array_element_dump
,此函数先输出键名,我们可以从它的注释看出来。分为数字型键名和string型键名,接着调用之前的方法,传递zval,根据类型不同输出不同格式。直到ZEND_HASH_FOREACH_END()
结束。
1 2 3 4 5 6 7 8 9 10 11 static void zval_array_element_dump (zval *zv, zend_ulong index, zend_string *key, int level) { if (key == NULL ) { php_printf("%*c[" ZEND_LONG_FMT "]=>\n" , level + 1 , ' ' , index); } else { php_printf("%*c[\"" , level + 1 , ' ' ); PHPWRITE(ZSTR_VAL(key), ZSTR_LEN(key)); php_printf("\"]=>\n" ); } php_debug_zval_dump(zv, level + 2 ); }
关于case IS_ARRAY
的部分大致上结束了,其中部分关于GC
的逻辑代码我没有探究,推测是垃圾回收的一些判断。
OBJECT到RESOURCE 接下来的case IS_OBJECT
也是差不多的逻辑。case IS_RESOURCE
也是短短几行,非常容易举一反三。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 case IS_OBJECT: myht = zend_get_properties_for(struc, ZEND_PROP_PURPOSE_DEBUG); if (myht) { if (GC_IS_RECURSIVE(myht)) { PUTS("*RECURSION*\n" ); zend_release_properties(myht); return ; } GC_PROTECT_RECURSION(myht); } class_name = Z_OBJ_HANDLER_P(struc, get_class_name)(Z_OBJ_P(struc)); php_printf("%sobject(%s)#%d (%d) refcount(%u){\n" , COMMON, ZSTR_VAL(class_name), Z_OBJ_HANDLE_P(struc), myht ? zend_array_count(myht) : 0 , Z_REFCOUNT_P(struc)); zend_string_release_ex(class_name, 0 ); if (myht) { ZEND_HASH_FOREACH_KEY_VAL(myht, index, key, val) { zend_property_info *prop_info = NULL ; if (Z_TYPE_P(val) == IS_INDIRECT) { val = Z_INDIRECT_P(val); if (key) { prop_info = zend_get_typed_property_info_for_slot(Z_OBJ_P(struc), val); } } if (!Z_ISUNDEF_P(val) || prop_info) { zval_object_property_dump(prop_info, val, index, key, level); } } ZEND_HASH_FOREACH_END(); GC_UNPROTECT_RECURSION(myht); zend_release_properties(myht); } if (level > 1 ) { php_printf("%*c" , level - 1 , ' ' ); } PUTS("}\n" ); break ; case IS_RESOURCE: { const char *type_name = zend_rsrc_list_get_rsrc_type(Z_RES_P(struc)); php_printf("%sresource(%d) of type (%s) refcount(%u)\n" , COMMON, Z_RES_P(struc)->handle, type_name ? type_name : "Unknown" , Z_REFCOUNT_P(struc)); break ; }
REFERENCE(答案) 我们来看case IS_REFERENCE
,它直接隐藏了真实的引用计数,当一个类型变为引用类型,它的引用计数在这个函数中只会输出1。在注释中我们也可以看出,它这样做是为了兼容性。
1 2 3 4 5 6 7 8 9 10 11 12 case IS_REFERENCE: if (Z_REFCOUNT_P(struc) > 1 ) { is_ref = 1 ; } struc = Z_REFVAL_P(struc); goto again; default : php_printf("%sUNKNOWN:0\n" , COMMON); break ; } }
总结 至此,我们看完了这个函数的源码。在这个函数中,对于没有引用计数的类型(LONG,FALSE,TRUE,DOUBLE,NULL
)仅仅输出它的值。
对于有引用计数的类型(STRING,ARRAY,OBJECT,RESOURCE
)根据逻辑判断,输出存放在zval结构中的refcount的值。
对于引用类型,则是用原有的数据重新判断输出。
xdebug_debug_zval和debug_zval_dump的区别 我们来看两段代码的运行结果:
1 2 3 4 5 6 7 8 9 <?php $a = "hello" ; debug_zval_dump($a); xdebug_debug_zval('a' );
一个字符串的默认引用计数为1。在上面这段代码的输出结果中,debug_zval_dump
的结果是符合预期的。而xdebug输出的interned
是什么意思?我们先保留疑问,再敲一段代码。
接下来这段代码输出的类型也是字符串:
1 2 3 4 5 6 7 8 9 <?php $a = "timestamp is : " . time(); debug_zval_dump($a); xdebug_debug_zval('a' );
为什么debug_zval_dump
输出2?而xdebug这次的输出结果却是符合预期的?!
深入探究 Interned String 在PHP5.4的时候, 引入了Interned String机制, 用于优化PHP对字符串的存储和处理。
Interned String 可以包括变量名称、类名、方法名、字符串、注释 等。第一段代码中的$a = 'hello'
,在多次编译下结果都不会产生变化。它们的生存周期存在于整个请求期间,请求结束后会统一销毁释放 ,自然也就无需通过引用计数进行内存管理。
Immutable array不可变数组 所有多次编译结果恒定不变的数组,都会被优化为不可变数组 ,是 opcache
扩展优化出的一种数组类型,多次编译结果不会产生变化,便会放入到OPcache里面进行优化。
函数调用增加引用计数 函数调用会增加引用计数,但在函数调用完成之后,函数内部变量销毁,引用计数减少到保持原有的值。而debug_zval_dump 这是一个php函数,它的输出是在函数内部的,所以输出的是它增加的引用计数。
总结
debug_zval_dump是PHP函数,函数调用会增加引用,而的输出正是在函数内部的,所以输出的引用计数是增加过的。
使用xdebug_debug_zval是正确的,但需要理解Interned String
和 Immutable array
的概念,这些内容可以在参考文章中详细了解。
学会GDB调试,逐步理解PHP源码执行过程
猜测,推导,证明的过程。所以要不断动手尝试,不能浮于表面。
以上的内容还涉及到了写时复制、写时分离、引用计数等等,每一个概念在PHP7的实现都值得深入探究。
参考文章 PHP7各种数据类型的引用计数
OPcache工作原理