0

0

PHP数组和值到底有多大

藏色散人

藏色散人

发布时间:2019-01-28 16:03:33

|

3600人浏览过

|

来源于php中文网

原创

这篇文章是关于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的额外因数是怎么来的呢?

立即学习PHP免费学习笔记(深入)”;

总结

对于那些不想知道整个故事的人,这里有一个涉及到的不同组件的内存使用的快速总结:

                             |  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。

Demila数字内容交易系统
Demila数字内容交易系统

感谢使用Demila。Demila是一款由9秒社团原创的、面向数字内容及其服务的、优美的在线交易系统,她除了能为站长提供一个建站解决方案之外,还能为那些技术精湛的设计者、开发者以及资源拥有者们提供无数个拓展其作品和资源之价值的机会,从而使那些精益求精者的智慧和创意,无论是价格,还是价值,都达到应有的尺度。这是9秒社团Demila项目组的宗旨,也是Demila的使命。我们热切希望能有更多的人为Dem

下载

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文件怎么打开

打开php文件步骤:1、选择文本编辑器;2、在选择的文本编辑器中,创建一个新的文件,并将其保存为.php文件;3、在创建的PHP文件中,编写PHP代码;4、要在本地计算机上运行PHP文件,需要设置一个服务器环境;5、安装服务器环境后,需要将PHP文件放入服务器目录中;6、一旦将PHP文件放入服务器目录中,就可以通过浏览器来运行它。

1728

2023.09.01

php怎么取出数组的前几个元素
php怎么取出数组的前几个元素

取出php数组的前几个元素的方法有使用array_slice()函数、使用array_splice()函数、使用循环遍历、使用array_slice()函数和array_values()函数等。本专题为大家提供php数组相关的文章、下载、课程内容,供大家免费下载体验。

1154

2023.10.11

php反序列化失败怎么办
php反序列化失败怎么办

php反序列化失败的解决办法检查序列化数据。检查类定义、检查错误日志、更新PHP版本和应用安全措施等。本专题为大家提供php反序列化相关的文章、下载、课程内容,供大家免费下载体验。

1056

2023.10.11

php怎么连接mssql数据库
php怎么连接mssql数据库

连接方法:1、通过mssql_系列函数;2、通过sqlsrv_系列函数;3、通过odbc方式连接;4、通过PDO方式;5、通过COM方式连接。想了解php怎么连接mssql数据库的详细内容,可以访问下面的文章。

948

2023.10.23

php连接mssql数据库的方法
php连接mssql数据库的方法

php连接mssql数据库的方法有使用PHP的MSSQL扩展、使用PDO等。想了解更多php连接mssql数据库相关内容,可以阅读本专题下面的文章。

1396

2023.10.23

html怎么上传
html怎么上传

html通过使用HTML表单、JavaScript和PHP上传。更多关于html的问题详细请看本专题下面的文章。php中文网欢迎大家前来学习。

1228

2023.11.03

PHP出现乱码怎么解决
PHP出现乱码怎么解决

PHP出现乱码可以通过修改PHP文件头部的字符编码设置、检查PHP文件的编码格式、检查数据库连接设置和检查HTML页面的字符编码设置来解决。更多关于php乱码的问题详情请看本专题下面的文章。php中文网欢迎大家前来学习。

1439

2023.11.09

php文件怎么在手机上打开
php文件怎么在手机上打开

php文件在手机上打开需要在手机上搭建一个能够运行php的服务器环境,并将php文件上传到服务器上。再在手机上的浏览器中输入服务器的IP地址或域名,加上php文件的路径,即可打开php文件并查看其内容。更多关于php相关问题,详情请看本专题下面的文章。php中文网欢迎大家前来学习。

1303

2023.11.13

虚拟号码教程汇总
虚拟号码教程汇总

本专题整合了虚拟号码接收验证码相关教程,阅读下面的文章了解更多详细操作。

25

2025.12.25

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
PHP基础视频教程
PHP基础视频教程

共40课时 | 9.3万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号