首頁  >  文章  >  後端開發  >  PHP數組和值到底有多大

PHP數組和值到底有多大

藏色散人
藏色散人原創
2019-01-28 16:03:333371瀏覽

這篇文章是關於PHP 5的記憶體使用情況。對於本文所述的情況,PHP 7的記憶體使用量大約低3倍。

PHP數組和值到底有多大

在這篇文章中,我想以下面的腳本為例來研究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, &#39; bytes&#39;;

它基本上做的是相同的事情,但是如果运行它,您会注意到它只使用了“5600640字节”。这是每个元素56字节,因此比普通数组使用的每个元素144字节要少得多。这是因为一个固定的数组不需要bucket结构:所以它只需要每个元素一个zval(48字节)和一个指针(8字节),从而得到观察到的56字节。

以上是PHP數組和值到底有多大的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn