介绍了array在PHP中的结构(zval):HashTable.
然后罗列了若干test case,从不同角度去理解foreach的机制:
reset();
while (get_current_data(&data) == SUCCESS) {
move_forward();
code();
}
结论是:
1) 数组的refcount__gc为1,is_ref__gc为0,那么foreach并不会复制zval;
2) 数组的refcount__gc>1,is_ref__gc为0,那么foreach将会复制zval;
3) 数组的is_ref__gc为1,那么foreach并不会复制zval;
看完文章之后,我对结论 2) 提出了更深层次的疑问,也就是作者文中的[附加的一个问题]:
有人问道当foreach发生zval复制时,从上面的例子可以得出这样的结论:(zval).value->ht会被复制一份,那么(zval).value->ht->arBuckets即该二级指针存储的Bucket是否也会被复制?
凭直觉,arBuckets是不应该被复制的,因为它的值并未发生变化,被复制的应该只是ht,而且复制整个数组其实开销非常大,当然这只是我的直觉罢了。
但是作者在文中使用了一个test case证明arBuckets也被复制了。可是我总觉得他用的这些test case有问题,最近忙着辞职,也没细想。
今天早上在路上又想了一下,其实可以使用memory_get_usage()函数来不断监测php脚本占用内存的情况, 从而来监测是否发生了arBuckets的复制。
在实验的过程中,发现,作者使用current()函数对foreach的分析都是错误的,zval的复制与否,并不是由foreach控制的,而是由current()控制的。
实验环境:
64位 php5.3.10
首先来看只有foreach的时候, 内存的使用情况:
print "INIT:".memory_get_usage().PHP_EOL;
$arr = range(1,2000);
print "FIRST ARRAY:".memory_get_usage().PHP_EOL;
$ref = $arr;
print "ref_count+1:".memory_get_usage().PHP_EOL;
foreach ($arr as $val)
{
echo $val;
}
print PHP_EOL;
print "after foreach:".memory_get_usage().PHP_EOL;
print PHP_EOL;
输出:
INIT:626288
FIRST ARRAY:915000 //第一个数组创建出来之后,内存增加200多K。
ref_count+1:915096 //$ref = $arr, 只是ref_count+1, ht并未被复制,arBuckets也没有被复制,内存增加 96字节,可能是消耗在全局符号表上面了(求详细解释)。
after foreach:915192 //foreach循环结束,内存增加96字节, 也是消耗在全局符号表上($val这个变量)。 循环结束之后,$val这个变量依然存在。
由此可见,foreach并没有导致zval:ht的复制。
再来看current():
print "INIT:".memory_get_usage().PHP_EOL;
$arr = range(1,2000);
print "FIRST ARRAY:".memory_get_usage().PHP_EOL;
$ref = $arr;
print "ref_count+1:".memory_get_usage().PHP_EOL;
var_dump(current($arr));
print "after current:".memory_get_usage().PHP_EOL;
输出:
INIT:625504
FIRST ARRAY:914200 //第一个数组创建出来之后,内存增加288696。
ref_count+1:914296 //$ref = $arr,ref_count+1 , 内存增加96字节。
int(1) //current()返回1, 正确。
after current:1106832 //current()之后,内存增加192536,可见发生了很多的复制...可是为什么会比288696要小呢? 相差96160。
current() 进行了一些复制,但是并没有把数组进行完全的复制,下面我就来猜测一下,究竟复制了数组的哪些部分。
1) 首先,zval ht肯定是被复制了。 zval占用48字节。
2) 其次,zval中的hash表:Bucket **arBuckets 被复制了,看一下Bucket的结构:
typedef struct bucket {
ulong h; // The hash (or for int keys the key) 8字节
uint nKeyLength; // The length of the key (for string keys) 4字节
void *pData; // The actual data 8字节
void *pDataPtr; // ??? What's this ??? 8字节
struct bucket *pListNext; // PHP arrays are ordered. This gives the next element in that order 8字节
struct bucket *pListLast; // and this gives the previous element 8字节
struct bucket *pNext; // The next element in this (doubly) linked list 8字节
struct bucket *pLast; // The previous element in this (doubly) linked list 8字节
const char *arKey; // The key (for string keys) 8字节
} Bucket;
共占用72字节,再加上16字节的mm信息,共占用88字节。 2000个数组元素就是 2000 * 88 = 176000字节。
考虑一下hash表的结构,还需要若干指针指向每一条链表的头。 假定每条链表中只有一个元素(这样hash表的查找效果最高,PHP应该能做到的),还需要2000个链表的头指针,共2000*8 = 16000字节。
176000 + 16000 = 192000,非常接近于192536字节了。
然后再反过来想一想,数组中的哪些部分没有被复制:
应该就是bucket中的 pData指向的zval没有被复制,即The actual data.
每个zval占用48字节, 2000个数组元素占用2000*48 = 96000字节,非常接近于96160。
感觉这样应该是正确的。
所以本文中曾经讲过的一句话:
凭直觉,arBuckets是不应该被复制的,因为它的值并未发生变化,被复制的应该只是ht,而且复制整个数组其实开销非常大,当然这只是我的直觉罢了。
应该纠正为:
凭直觉, ht被复制,arBuckets被复制,但是每个Bucket指向的zval没有被复制(这些zval的ref_count会加1)。
补充:
在current()时,zval分离,完成复制之后, 二个变量的ht的内部指针pInternalPointer被自动reset。
补充2:
通过在foreach循环内部插入 memory_get_usage(), 可以看到在foreach循环内部,内存增加192632字节,foreach结束之后,内存减少192536字节,说明在foreach内部还是发生了ht的复制,原作者的结论是正确的。