這篇文章是關於PHP 5的記憶體使用情況。對於本文所述的情況,PHP 7的記憶體使用量大約低3倍。
在這篇文章中,我想以下面的腳本為例來研究PHP數組(以及一般的值)的記憶體使用情況,該腳本創建了100000個惟一的整數數組元素,並測量了結果的內存使用情況:
$startMemory = memory_get_usage(); $array = range(1, 100000); echo memory_get_usage() - $startMemory, ' bytes';
你希望它是多少?簡單來說,一個整數是8字節(在64位unix機器上使用long類型),您得到100,000個整數,因此顯然需要800000位元組。
現在嘗試運行上面的程式碼。這就得到了14649024位元組。是的,你沒聽錯,是13.97 MB,比我們估計的多18倍。
那麼,18的額外因數是怎麼來的呢?
總結
#對於那些不想知道整個故事的人,這裡有一個涉及到的不同組件的記憶體使用的快速總結:
| 64 bit | 32 bit --------------------------------------------------- zval | 24 bytes | 16 bytes + cyclic GC info | 8 bytes | 4 bytes + allocation header | 16 bytes | 8 bytes =================================================== zval (value) total | 48 bytes | 28 bytes =================================================== bucket | 72 bytes | 36 bytes + allocation header | 16 bytes | 8 bytes + pointer | 8 bytes | 4 bytes =================================================== bucket (array element) total | 96 bytes | 48 bytes =================================================== total total | 144 bytes | 76 bytes
上述數字將根據您的作業系統、編譯器和編譯選項的不同而有所不同。例如,如果您使用偵錯或線程安全性來編譯PHP,您將得到不同的數字。但是我認為上面給出的大小是您將在Linux上的PHP 5.3的64位元生產版本中看到的大小。
如果你用這144位元組乘以100000個元素,你會得到14400000字節,也就是13.73 MB,這與實際數字非常接近——剩下的大部分都是未初始化bucket的指針,但是我將在後面討論這個問題。
現在,如果您想對上面提到的值進行更詳細的分析,請繼續閱讀:)
zvalue_value聯盟
首先看看PHP是如何儲存值的。正如您所知道的,PHP是一種弱類型語言,因此它需要某種方式在各種類型之間快速切換。 PHP為此使用union,它在zend中定義如下。
typedef union _zvalue_value { long lval; // For integers and booleans double dval; // For floats (doubles) struct { // For strings char *val; // consisting of the string itself int len; // and its length } str; HashTable *ht; // For arrays (hash tables) zend_object_value obj; // For objects } zvalue_value;
如果您不知道C,這不是一個問題,因為程式碼非常簡單:union是一種使某些值可以作為各種類型存取的方法。例如,如果您執行zvalue_value->lval,您將得到一個被解釋為整數的值。另一方面,如果您使用zvalue_value->ht,則該值將被解釋為指向哈希表(即數組)的指標。
但我們不要在這裡講太多。對我們來說,唯一重要的是一個union的大小等於它的最大組件的大小。這裡最大的元件是字串結構體(zend_object_value結構體的大小與str結構體相同,但為了簡單起見,我將省略它)。 string struct儲存一個指標(8位元組)和一個整數(4位元組),總共是12位元組。由於記憶體對齊(12位元組的結構並不酷,因為它們不是64位元/ 8位元組的倍數),結構的總大小將是16位元組,這也是union作為一個整體的大小。
現在我們知道,由於PHP的動態類型,每個值不需要8位元組,而是16位元組。乘以100000個值得到1600000字節,也就是1.53 MB,但是實際的值是13.97 MB,所以我們還不能得到它。
zval的結構
這非常符合邏輯-union只儲存值本身,但PHP顯然還需要儲存型別和一些垃圾收集資訊。保存此資訊的結構稱為zval,您可能已經聽說過它。關於PHP為什麼需要它的更多信息,我建議閱讀Sara Golemon的一篇文章。無論如何,這個結構的定義如下:
struct _zval_struct { zvalue_value value; // The value zend_uint refcount__gc; // The number of references to this value (for GC) zend_uchar type; // The type zend_uchar is_ref__gc; // Whether this value is a reference (&) };
結構的大小由其組件的大小之和決定:zvalue_value為16字節(如上所計算),zend_uint為4字節,zend_uchars為1字節。總共是22位元組。由於記憶體對齊,實際大小將是24位元組。
因此,如果我們儲存100,000個元素a 24字節,那麼總共就是2400000,也就是2.29 MB,差距正在縮小,但是實際值仍然是原來的6倍多。
循環收集器(從PHP 5.3開始)
PHP 5.3引進了一個新的循環引用垃圾收集器。為此,PHP必須儲存一些額外的資料。我不想在這裡解釋這個演算法是如何運作的,你可以在手冊的連結頁面上讀到。對於我們的大小計算來說,重要的是PHP將把每個zval包裝成zval_gc_info:
typedef struct _zval_gc_info { zval z; union { gc_root_buffer *buffered; struct _zval_gc_info *next; } u; } zval_gc_info;
正如您所看到的,Zend只在它上面添加了一個union,它由兩個指針組成。希望您還記得,union的大小就是它最大的元件的大小:兩個union元件都是指針,因此它們的大小都是8位元組。所以union的大小也是8位元組。
如果我們把它加到24位元組上面我們已經有32位元組了。再乘以100000個元素,我們得到的記憶體使用量是3。05 MB。
Zend MM分配器
C與PHP不同,它不會為您管理記憶體。你需要自己記錄你的分配。為此,PHP使用了專門針對其需要最佳化的自訂記憶體管理器:Zend記憶體管理器。 Zend MM基於Doug Lea的malloc,並添加了一些PHP特有的優化和特性(如內存限制、每次請求後清理等)。
這裡對我們來說重要的是,MM為透過它完成的每個分配添加一個分配頭。定義如下:
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; typedef struct _zend_mm_block_info { #if ZEND_MM_COOKIES size_t _cookie; #endif size_t _size; // size of the allocation size_t _prev; // previous block (not sure what exactly this is) } zend_mm_block_info;
如您所见,这些定义充斥着大量的编译选项检查。如果你用堆保护,多线程,调试和MM cookie来构建PHP,那么如果你用堆保护,多线程,调试和MM cookie来构建PHP,那么如果你用堆保护,多线程,调试和MM cookie来构建PHP,那么分配头文件会更大。
对于本例,我们假设所有这些选项都是禁用的。在这种情况下,只剩下两个size_ts _size和_prev。size_t有8个字节(在64位上),所以分配头的总大小是16个字节——并且在每个分配上都添加了这个头。
现在我们需要再次调整zval大小。实际上,它不是32字节,而是48字节,这是由分配头决定的。乘以100000个元素是4。58 MB,实际值是13。97 MB,所以我们已经得到了大约三分之一的面积。
Buckets
到目前为止,我们只考虑单个值。但是PHP中的数组结构也会占用大量空间:“数组”在这里实际上是一个不合适的术语。PHP数组实际上是散列表/字典。那么哈希表是如何工作的呢?基本上,对于每个键,都会生成一个散列,该散列用作“real”C数组的偏移量。由于哈希值可能会冲突,具有相同哈希值的所有元素都存储在链表中。当访问一个元素时,PHP首先计算散列,查找正确的bucket并遍历链接列表,逐个元素比较确切的键。bucket的定义如下:
typedef struct bucket { ulong h; // The hash (or for int keys the key) uint nKeyLength; // The length of the key (for string keys) void *pData; // The actual data void *pDataPtr; // ??? What's this ??? struct bucket *pListNext; // PHP arrays are ordered. This gives the next element in that order struct bucket *pListLast; // and this gives the previous element struct bucket *pNext; // The next element in this (doubly) linked list struct bucket *pLast; // The previous element in this (doubly) linked list const char *arKey; // The key (for string keys) } Bucket;
正如您所看到的,需要存储大量数据才能获得PHP使用的抽象数组数据结构(PHP数组同时是数组、字典和链表,这当然需要大量信息)。单个组件的大小为无符号long为8字节,无符号int为4字节,指针为7乘以8字节。总共是68。添加对齐,得到72字节。
像zvals这样的bucket需要在头部分配,因此我们需要再次为分配头添加16个字节,从而得到88个字节。我们还需要在“real”C数组中存储指向这些Bucket的指针(Bucket ** arbucket;)我上面提到过,每个元素增加8个字节。所以总的来说,每个bucket需要96字节的存储空间。
如果每个值都需要一个bucket,那么bucket是96字节,zval是48字节,总共144字节。对于100000个元素,也就是14400000字节,即13.73 MB。
神秘的解决。
等等,还有0.24 MB !
最后的0.24 MB是由于未初始化的存储bucket造成的:理想情况下,存储bucket的实际C数组的大小应该与存储的数组元素的数量大致相同。通过这种方式,冲突最少(除非希望浪费大量内存)。但是PHP显然不能在每次添加元素时重新分配整个数组——这将非常缓慢。相反,如果内部bucket数组达到限制,PHP总是将其大小加倍。所以数组的大小总是2的幂。
在我们的例子中是2 ^ 17 = 131072。但是我们只需要100000个bucket,所以我们留下31072个bucket没有使用。这些bucket不会被分配(因此我们不需要花费全部的96字节),但是bucket指针(存储在内部桶数组中的那个)的内存仍然需要分配。所以我们另外使用8字节(一个指针)* 31072个元素。这是248576字节或0.23 MB,与丢失的内存匹配。(当然,这里仍然缺少一些字节,但是我不想在这里介绍。比如哈希表结构本身,变量等等)
神秘真的解决了。
这告诉我们什么?
PHP不是c,这就是所有这些告诉我们的。您不能期望像PHP这样的超级动态语言具有与C语言相同的高效内存使用。你不能。
但是,如果您确实想节省内存,可以考虑使用SplFixedArray处理大型静态数组。
看看这个修改后的脚本:
$startMemory = memory_get_usage(); $array = new SplFixedArray(100000); for ($i = 0; $i < 100000; ++$i) { $array[$i] = $i; } echo memory_get_usage() - $startMemory, ' bytes';
它基本上做的是相同的事情,但是如果运行它,您会注意到它只使用了“5600640字节”。这是每个元素56字节,因此比普通数组使用的每个元素144字节要少得多。这是因为一个固定的数组不需要bucket结构:所以它只需要每个元素一个zval(48字节)和一个指针(8字节),从而得到观察到的56字节。
以上是PHP數組和值到底有多大的詳細內容。更多資訊請關注PHP中文網其他相關文章!