这篇文章是关于PHP 5的内存使用情况。对于本文所述的情况,PHP 7中的内存使用量大约低3倍。

PHP数组和值到底有多大-LMLPHP

在这篇文章中,我想以下面的脚本为例来研究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数组和值到底有多大的详细内容,更多请关注Work网其它相关文章!

08-18 19:14