时间:2021-07-01 10:21:17 帮助过:66人阅读
在这篇文章中,我想以下面的脚本为例来研究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数组和值到底有多大的详细内容,更多请关注Gxl网其它相关文章!