介紹了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的復制,原作者的結論是正確的。