PHP数组实际占用内存大小的分析与弱语言

我们在前面的php高效写法提到,尽量不要复制变量,特别是数组。一般来说,PHP数组的内存利用率只有 1/10, 也就是说,一个在C语言里面100M 内存的数组,在PHP里面就要1G。下面我们可以粗略的估算PHP数组占用内存的大小,首先我们测试1000个元素的整数占用的内存:

  1. <?php  
  2.     echo memory_get_usage() , '<br>';  
  3.     $start = memory_get_usage();  
  4.     $a = Array();  
  5.     for ($i=0; $i<1000; $i++) {  
  6.     $a[$i] = $i + $i;  
  7.     }  
  8.     $mid =  memory_get_usage();  
  9.     echo memory_get_usage() , '<br>';  
  10.     for ($i=1000; $i<2000; $i++) {  
  11.     $a[$i] = $i + $i;  
  12.     }  
  13.     $end =  memory_get_usage();  
  14.     echo memory_get_usage() , '<br>';  
  15.     echo 'argv:', ($mid - $start)/1000 ,'bytes' , '<br>';  
  16.     echo 'argv:',($end - $mid)/1000 ,'bytes' , '<br>';  

输出是:

353352
    437848
    522024
    argv:84.416bytes
    argv:84.176bytes

大概了解1000 个元素的整数数组需要占用 82k 内存,平均每个元素占用 84 个字节。而纯 C 中整体只需要 4k(一个整型占用4byte * 1000 )。memory_get_usage() 返回的结果并不是全是被数组占用了,还要包括一些 PHP 运行本身分配的一些结构,可能用内置函数生成的数组更接近真实的空间:

  1. <?php  
  2.     $start = memory_get_usage();  
  3.     $a = array_fill(0, 10000, 1);  
  4.     $mid = memory_get_usage(); //10k elements array;  
  5.     echo 'argv:', ($mid - $start )/10000,'byte' , '<br>';  
  6.     $b = array_fill(0, 10000, 1);  
  7.     $end = memory_get_usage(); //10k elements array;  
  8.     echo 'argv:', ($end - $mid)/10000 ,'byte' , '<br>';  

得到:
argv:54.5792byte
argv:54.5784byte

从这个结果来看似乎一个数组元素大约占用了54个字节左右。

首先看一下32位机C语言各种类型占用的字节:

  1. #include "stdafx.h"  
  2. //#include <stdio.h>  
  3.    
  4. int main() {  
  5.         printf("int:%d\nlong:%d\ndouble:%d\nchar*:%d\nsize_t:%d\n",   
  6.         sizeof(int), sizeof(long),   
  7.         sizeof(double), sizeof(char *),   
  8.         sizeof(size_t));  
  9.     return   0;   
  10. }  


PHP数组实际占用内存大小的分析与弱语言_第1张图片


int:4
long:4
double:8
har*:4
size_t:4
在PHP中都使用long类型来代表数字,没有使用int类型
大家都明白PHP是一种弱类型的语言,它不会去区分变量的类型,没有int float char *之类的概念。
我们看看php在zend里面存储的变量,PHP中每个变量都有对应的 zval, Zval结构体定义在Zend/zend.h里面,其结构:

  1. typedef struct _zval_struct zval;  
  2. struct _zval_struct {  
  3.     /* Variable information */  
  4.     zvalue_value value;     /* The value 1 12字节(32位机是12,64位机需要8+4+4=16) */  
  5.     zend_uint refcount__gc; /* The number of references to this value (for GC) 4字节 */  
  6.     zend_uchar type;        /* The active type 1字节*/  
  7.     zend_uchar is_ref__gc;  /* Whether this value is a reference (&) 1字节*/  
  8. };  

PHP使用一种UNION结构来存储变量的值,即zvalue_value 是一个union,UNION变量所占用的内存是由最大

成员数据空间决定。

  1. typedef union _zvalue_value {  
  2.     long lval;                  /* long value */  
  3.     double dval;                /* double value */  
  4.     struct {                    /* string value */  
  5.         char *val;  
  6.         int len;  
  7.     } str;   
  8.     HashTable *ht;              /* hash table value */  
  9.     zend_object_value obj;      /*object value */  
  10. } zvalue_value;  

 最大成员数据空间是struct str,指针占*val用4字节,INT占用4字节,共8字节。

       struct zval占用的空间为8+4+1+1 = 14字节,

      其实呢,在zval中数组,字符串和对象还需要另外的存储结构,数组则是一个 HashTable:

   HashTable结构体定义在Zend/zend_hash.h.

  1. typedef struct _hashtable {  
  2.     uint nTableSize;//4  
  3.     uint nTableMask;//4  
  4.     uint nNumOfElements;//4  
  5.     ulong nNextFreeElement;//4  
  6.     Bucket *pInternalPointer;   /* Used for element traversal 4*/  
  7.     Bucket *pListHead;//4  
  8.     Bucket *pListTail;//4  
  9.     Bucket **arBuckets;//4  
  10.     dtor_func_t pDestructor;//4  
  11.     zend_bool persistent;//1  
  12.     unsigned char nApplyCount;//1  
  13.     zend_bool bApplyProtection;//1  
  14. #if ZEND_DEBUG  
  15.     int inconsistent;//4  
  16. #endif  
  17. } HashTable;  
HashTable 结构需要 39 个字节,每个数组元素存储在 Bucket 结构中:

  1. typedef struct bucket {  
  2.     ulong h;    /* Used for numeric indexing                4字节 */  
  3.     uint nKeyLength;    /* The length of the key (for string keys)  4字节 */  
  4.     void *pData;        /* 4字节*/  
  5.     void *pDataPtr;         /* 4字节*/  
  6.     struct bucket *pListNext;  /* PHP arrays are ordered. This gives the next element in that order4字节*/  
  7.     struct bucket *pListLast;  /* and this gives the previous element           4字节 */  
  8.     struct bucket *pNext;      /* The next element in this (doubly) linked list     4字节*/  
  9.     struct bucket *pLast;      /* The previous element in this (doubly) linked list     4字节*/  
  10.     char arKey[1];            /* Must be last element   1字节*/  
  11. } Bucket;  

Bucket 结构需要 33 个字节,键长超过四个字节的部分附加在 Bucket 后面,而元素值很可能是一个 zval 结构,另外每个数组会分配一个由 arBuckets 指向的 Bucket 指针数组, 虽然不能说每增加一个元素就需要一个指针,但是实际情况可能更糟。这么算来一个数组元素就会占用 54 个字节,与上面的估算几乎一样。

    一个空数组至少会占用 14(zval) + 39(HashTable) + 33(arBuckets) = 86 个字节,作为一个变量应该在符号表中有个位置,也是一个数组元素,因此一个空数组变量需要 118 个字节来描述和存储。从空间的角度来看,小型数组平均代价较大,当然一个脚本中不会充斥数量很大的小型数组,可以以较小的空间代价来获取编程上的快捷。但如果将数组当作容器来使用就是另一番景象了,实际应用经常会遇到多维数组,而且元素居多。比如10k个元素的一维数组大概消耗540k内存,而10k x 10 的二维数组理论上只需要 6M 左右的空间,但是按照 memory_get_usage 的结果则两倍于此,[10k,5,2]的三维数组居然消耗了23M,小型数组果然是划不来的



###------------------------------------------------------------###

或许你知道,或许你不知道,PHP是一个弱类型,动态的脚本语言。所谓弱类型,就是说PHP并不严格验证变量类型(严格来讲,PHP是一个中强类型语言),在申明一个变量的时候,并不需要显示指明它保存的数据的类型:

?
1
2
3
4
5
6
<?php  
   $var = 1 ; / / int  
   $var = "laruence" ; / / string  
   $var = 1.0002 ; / / float  
   $var = array(); / / array  
   $var = new Exception( 'error' ); / / object

动态语言,就是说,PHP的语言结构在运行期是可以改变的,比如我们在运行期require一个函数定义文件,从而导致语言的函数表动态的改变。

所谓脚本语言,就是说,PHP并不是独立运行的,要运行PHP我们需要PHP解析器:

/usr/bin/php -f example.php PHP的执行是通过Zend engine(ZE, Zend引擎), ZE是用C编写的,大家都知道C是一个强类型语言,也就是说,在C中所有的变量在它被声明到最终销毁,都只能保存一种类型的数据。 那么PHP是如何在ZE的基础上实现弱类型的呢?

在PHP中,所有的变量都是用一个结构-zval来保存的, 在Zend/zend.h中我们可以看到zval的定义:

?
1
2
3
4
5
6
typedef struct _zval_struct {  
      zvalue_value value;  
      zend_uint refcount;  
      zend_uchar type ;  
      zend_uchar is_ref;  
    } zval; 

其中zvalue_value是真正保存数据的关键部分,现在到了揭晓谜底的时候了,PHP是如何在ZE的基础上实现弱类型的呢? 因为zvalue_value是个联合体(union)。

?
1
2
3
4
5
6
7
8
9
10
typedef union _zvalue_value {  
      lval;  
      dval;  
     struct {  
          * val;  
          len ;  
      } str ;  
      HashTable * ht;  
      zend_object_value obj;  
} zvalue_value; 
  • 那么这个结构是如何储存PHP中的多种类型的呢?

PHP中常见的变量类型有:

1. 整型/浮点/长整型/bool值 [[等等]]
2. 字符串
3. 数组/关联数组
4. 对象
5. 资源

PHP根据zval中的type字段来储存一个变量的真正类型,然后根据type来选择如何获取zvalue_value的值,比如对于整型和bool值: zval.type = IS_LONG;//整形 zval.type = IS_BOOL;//布尔值

就去取zval.value.lval,对于bool值来说lval∈(0|1); 如果是双精度,或者float则会去取zval.value的dval。 而如果是字符串,那么:

zval.type = IS_STRING

这个时候,就会取zval.value.str。而这个也是个结构,存有C分格的字符串和字符串的长度。 而对于数组和对象,则type分别对应IS_ARRAY, IS_OBJECT, 相对应的则分别取zval.value.ht和obj 比较特别的是资源,在PHP中,资源是个很特别的变量,任何不属于PHP内建的变量类型的变量,都会被看作成资源来进行保存,比如,数据库句柄,打开的文件句柄等等。 对于资源:

type = IS_RESOURCE

这个时候,会去取zval.value.lval, 此时的lval是个整型的指示器, 然后PHP会再根据这个指示器在PHP内建的一个资源列表中查询相对应的资源。目前,你只要知道此时的lval就好像是对应于资源链表的偏移值。

?
1
ZEND_FETCH_RESOURCE(con, type , zval * , default, resource_name, resource_type);

借用这样的机制,PHP就实现了弱类型,因为对于ZE的来说,它所面对的永远都是同一种类型,那就是zval。


 ###----------------------------------------------------------------------------------------------###

、 基础知识

  本章简要介绍一些Zend引擎的内部机制,这些知识和Extensions密切相关,同时也可以帮助我们写出更加高效的PHP代码。

  1.1 PHP变量的存储

  1.1.1 zval结构

  Zend使用zval结构来存储PHP变量的值,该结构如下所示:

  
  
  
  
  1. typedef union _zvalue_value { 
  2.     long lval;              /* long value */ 
  3.     double dval;                /* double value */ 
  4.     struct { 
  5.         char *val; 
  6.         int len; 
  7.     } str; 
  8.     HashTable *ht;              /* hash table value */ 
  9.     zend_object_value obj; 
  10. } zvalue_value; 
  11.  
  12. struct _zval_struct { 
  13.     /* Variable information */ 
  14.     zvalue_value value;     /* value */ 
  15.     zend_uint refcount; 
  16.     zend_uchar type;            /* active type */ 
  17.     zend_uchar is_ref; 
  18. }; 
  19.  
  20. typedef struct _zval_struct zval; 
  21. <span id="more-597"></span>Zend根据type值来决定访问value的哪个成员,可用值如下: 

 

  IS_NULLN/A

  IS_LONG对应value.lval

  IS_DOUBLE对应value.dval

  IS_STRING对应value.str

  IS_ARRAY对应value.ht

  IS_OBJECT对应value.obj

  IS_BOOL对应value.lval.

  IS_RESOURCE对应value.lval

  根据这个表格可以发现两个有意思的地方:首先是PHP的数组其实就是一个HashTable,这就解释了为什么PHP能够支持关联数组了;其次,Resource就是一个long值,它里面存放的通常是个指针、一个内部数组的index或者其它什么只有创建者自己才知道的东西,可以将其视作一个handle

  1.1.1 引用计数

  引用计数在垃圾收集、内存池以及字符串等地方应用广泛,Zend就实现了典型的引用计数。多个PHP变量可以通过引用计数机制来共享同一份zval,zval中剩余的两个成员is_ref和refcount就用来支持这种共享。

  很明显,refcount用于计数,当增减引用时,这个值也相应的递增和递减,一旦减到零,Zend就会回收该zval。

  那么is_ref呢?

  1.1.2 zval状态

  在PHP中,变量有两种——引用和非引用的,它们在Zend中都是采用引用计数的方式存储的。对于非引用型变量,要求变量间互不相干,修改一个变量时,不能影响到其他变量,采用Copy-On-Write机制即可解决这种冲突——当试图写入一个变量时,Zend若发现该变量指向的zval被多个变量共享,则为其复制一份refcount为1的zval,并递减原zval的refcount,这个过程称为“zval分离”。然而,对于引用型变量,其要求和非引用型相反,引用赋值的变量间必须是捆绑的,修改一个变量就修改了所有捆绑变量。

  可见,有必要指出当前zval的状态,以分别应对这两种情况,is_ref就是这个目的,它指出了当前所有指向该zval的变量是否是采用引用赋值的——要么全是引用,要么全不是。此时再修改一个变量,只有当发现其zval的is_ref为0,即非引用时,Zend才会执行Copy-On-Write。

  1.1.3 zval状态切换

  当在一个zval上进行的所有赋值操作都是引用或者都是非引用时,一个is_ref就足够应付了。然而,世界总不会那么美好,PHP无法对用户进行这种限制,当我们混合使用引用和非引用赋值时,就必须要进行特别处理了。

  情况I、看如下PHP代码:

  
  
  
  
  1. <!--p $a = 1;  $b = &$a;  $c = &$b;  $d = $c;  // 在一堆引用赋值中,插入一个非引用--> 

 

 

  全过程如下所示:

  这段代码的前三句将把a、b和c指向一个zval,其is_ref=1, refcount=3;第四句是个非引用赋值,通常情况下只需要增加引用计数即可,然而目标zval属于引用变量,单纯的增加引用计数显然是错误的, Zend的解决办法是为d单独生成一份zval副本。

  全过程如下所示:

  

PHP数组实际占用内存大小的分析与弱语言_第2张图片

 

  1.1.1 参数传递

  PHP函数参数的传递和变量赋值是一样的,非引用传递相当于非引用赋值,引用传递相当于引用赋值,并且也有可能会导致执行zval状态切换。这在后面还将提到。

  1.2 HashTable结构

  HashTable是Zend引擎中最重要、使用最广泛的数据结构,它被用来存储几乎所有的东西。

  1.1.1 数据结构

  HashTable数据结构定义如下:

  
  
  
  
  1. typedef struct bucket { 
  2.     ulong h;                // 存放hash 
  3.     uint nKeyLength; 
  4.     void *pData;            // 指向value,是用户数据的副本 
  5.     void *pDataPtr; 
  6.     struct bucket *pListNext;   // pListNext和pListLast组成 
  7.     struct bucket *pListLast;   // 整个HashTable的双链表 
  8.     struct bucket *pNext;       // pNext和pLast用于组成某个hash对应 
  9.     struct bucket *pLast;       // 的双链表 
  10.     char arKey[1];              // key 
  11. } Bucket; 
  12.  
  13. typedef struct _hashtable { 
  14.     uint nTableSize; 
  15.     uint nTableMask; 
  16.     uint nNumOfElements; 
  17.     ulong nNextFreeElement; 
  18.     Bucket *pInternalPointer;   /* Used for element traversal */ 
  19.     Bucket *pListHead; 
  20.     Bucket *pListTail; 
  21.     Bucket **arBuckets;         // hash数组 
  22.     dtor_func_t pDestructor;    // HashTable初始化时指定,销毁Bucket时调用 
  23.     zend_bool persistent;       // 是否采用C的内存分配例程 
  24.     unsigned char nApplyCount; 
  25.     zend_bool bApplyProtection; 
  26. #if ZEND_DEBUG 
  27.     int inconsistent; 
  28. #endif 
  29. } HashTable; 

  总的来说,Zend的HashTable是一种链表散列,同时也为线性遍历进行了优化,图示如下:


  HashTable中包含两种数据结构,一个链表散列和一个双向链表,前者用于进行快速键-值查询,后者方便线性遍历和排序,一个Bucket同时存在于这两个数据结构中。

  关于该数据结构的几点解释:

  l 链表散列中为什么使用双向链表?

  一般的链表散列只需要按key进行操作,只需要单链表就够了。但是,Zend有时需要从链表散列中删除给定的Bucket,使用双链表可以非常高效的实现。

  l nTableMask是干什么的?

  这个值用于hash值到arBuckets数组下标的转换。当初始化一个HashTable,Zend首先为arBuckets数组分配nTableSize大小的内存,nTableSize取不小于用户指定大小的最小的2^n,即二进制的10*。nTableMask = nTableSize – 1,即二进制的01*,此时h & nTableMask就恰好落在 [0, nTableSize – 1] 里,Zend就以其为index来访问arBuckets数组。

  l pDataPtr是干什么的?

  通常情况下,当用户插入一个键值对时,Zend会将value复制一份,并将pData指向value副本。复制操作需要调用Zend内部例程 emalloc来分配内存,这是个非常耗时的操作,并且会消耗比value大的一块内存(多出的内存用于存放cookie),如果value很小的话,将会造成较大的浪费。考虑到HashTable多用于存放指针值,于是Zend引入pDataPtr,当value小到和指针一样长时,Zend就直接将其复制到pDataPtr里,并且将pData指向pDataPtr。这就避免了emalloc操作,同时也有利于提高Cache命中率。

  arKey大小为什么只有1?为什么不使用指针管理key?

  arKey是存放key的数组,但其大小却只有1,并不足以放下key。在HashTable的初始化函数里可以找到如下代码:

  1p = (Bucket *) pemalloc(sizeof(Bucket) - 1 + nKeyLength, ht->persistent);

  可见,Zend为一个Bucket分配了一块足够放下自己和key的内存,

  l 上半部分是Bucket,下半部分是key,而arKey“恰好”是Bucket的最后一个元素,于是就可以使用arKey来访问key了。这种手法在内存管理例程中最为常见,当分配内存时,实际上是分配了比指定大小要大的内存,多出的上半部分通常被称为cookie,它存储了这块内存的信息,比如块大小、上一块指针、下一块指针等,baidu的Transmit程序就使用了这种方法。

  不用指针管理key,是为了减少一次emalloc操作,同时也可以提高Cache命中率。另一个必需的理由是,key绝大部分情况下是固定不变的,不会因为key变长了而导致重新分配整个Bucket。这同时也解释了为什么不把value也一起作为数组分配了——因为value是可变的。

  1.2.2 PHP数组

  关于HashTable还有一个疑问没有回答,就是nNextFreeElement是干什么的?

  不同于一般的散列,Zend的HashTable允许用户直接指定hash值,而忽略key,甚至可以不指定key(此时,nKeyLength为0)。同时,HashTable也支持append操作,用户连hash值也不用指定,只需要提供value,此时,Zend就用nNextFreeElement作为hash,之后将nNextFreeElement递增。

  HashTable的这种行为看起来很奇怪,因为这将无法按key访问value,已经完全不是个散列了。理解问题的关键在于,PHP数组就是使用HashTable实现的——关联数组使用正常的k-v映射将元素加入HashTable,其key为用户指定的字符串;非关联数组则直接使用数组下标作为hash值,不存在key;而当在一个数组中混合使用关联和非关联时,或者使用array_push操作时,就需要用nNextFreeElement了。

  再来看value,PHP数组的value直接使用了zval这个通用结构,pData指向的是zval*,按照上一节的介绍,这个zval*将直接存储在pDataPtr里。由于直接使用了zval,数组的元素可以是任意PHP类型。

  数组的遍历操作,即foreach、each等,是通过HashTable的双向链表来进行的,pInternalPointer作为游标记录了当前位置。

  1.2.3 变量符号表

  除了数组,HashTable还被用来存储许多其他数据,比如,PHP函数、变量符号、加载的模块、类成员等。

  一个变量符号表就相当于一个关联数组,其key是变量名(可见,使用很长的变量名并不是个好主意),value是zval*。

  在任一时刻PHP代码都可以看见两个变量符号表——symbol_table和active_symbol_table——前者用于存储全局变量,称为全局符号表;后者是个指针,指向当前活动的变量符号表,通常情况下就是全局符号表。但是,当每次进入一个PHP函数时(此处指的是用户使用PHP代码创建的函数),Zend都会创建函数局部的变量符号表,并将active_symbol_table指向局部符号表。Zend总是使用active_symbol_table来访问变量,这样就实现了局部变量的作用域控制。

  但如果在函数局部访问标记为global的变量,Zend会进行特殊处理——在active_symbol_table中创建symbol_table中同名变量的引用,如果symbol_table中没有同名变量则会先创建。

  1.3 内存和文件

  程序拥有的资源一般包括内存和文件,对于通常的程序,这些资源是面向进程的,当进程结束后,操作系统或C库会自动回收那些我们没有显式释放的资源。

  但是,PHP程序有其特殊性,它是基于页面的,一个页面运行时同样也会申请内存或文件这样的资源,然而当页面运行结束后,操作系统或C库也许不会知道需要进行资源回收。比如,我们将php作为模块编译到apache里,并且以prefork或worker模式运行apache。这种情况下apache进程或线程是复用的,php页面分配的内存将永驻内存直到出core。

  为了解决这种问题,Zend提供了一套内存分配API,它们的作用和C中相应函数一样,不同的是这些函数从Zend自己的内存池中分配内存,并且它们可以实现基于页面的自动回收。在我们的模块中,为页面分配的内存应该使用这些API,而不是C例程,否则Zend会在页面结束时尝试efree掉我们的内存,其结果通常就是crush。

  emalloc()

  efree()

  estrdup()

  estrndup()

  ecalloc()

  erealloc()

  另外,Zend还提供了一组形如VCWD_xxx的宏用于替代C库和操作系统相应的文件API,这些宏能够支持PHP的虚拟工作目录,在模块代码中应该总是使用它们。宏的具体定义参见PHP源代码”TSRM/tsrm_virtual_cwd.h”。可能你会注意到,所有那些宏中并没有提供close操作,这是因为close的对象是已打开的资源,不涉及到文件路径,因此可以直接使用C或操作系统例程;同理,read/write之类的操作也是直接使用C或操作系统的例程。



你可能感兴趣的:(PHP数组实际占用内存大小的分析与弱语言)