PHP内存实际上是如何工作的

时间:2014-08-01 13:27:12

标签: php arrays memory-management php-internals

我总是听到并搜索新的PHP“良好的写作练习”,例如:检查数组键是否存在比搜索数组更好(性能),但对内存来说似乎也更好:

假设我们有:

$array = array
(
    'one'   => 1,
    'two'   => 2,
    'three' => 3,
    'four'  => 4,
);

这会分配1040个字节的内存,

$array = array
(
    1 => 'one',
    2 => 'two',
    3 => 'three',
    4 => 'four',
);

需要1136个字节

我知道keyvalue肯定会有不同的存储机制,但是 请你真的指出我的原理是如何运作的?

示例2 (对于@teuneboon)

$array = array
(
    'one'   => '1',
    'two'   => '2',
    'three' => '3',
    'four'  => '4',
);

1168字节

$array = array
(
    '1' => 'one',
    '2' => 'two',
    '3' => 'three',
    '4' => 'four',
);

1136字节

消耗相同的内存:

  • 4 => 'four',
  • '4' => 'four',

4 个答案:

答案 0 :(得分:24)

注意,以下答案适用于PHP 之前到版本7,因为在PHP 7中引入了主要更改,其中还涉及值结构。

TL; DR

你的问题实际上并不是关于&#34;内存如何在PHP&#34; 中运行(在这里,我认为,你的意思是&#34;内存分配&#34;),但是关于< em>&#34;数组如何在PHP&#34; 中运行 - 这两个问题是不同的。总结下面的内容:

  • PHP数组不是&#34;数组&#34;在古典意义上。它们是哈希映射
  • PHP数组的哈希映射具有特定的结构,并使用许多其他存储内容,例如内部链接指针
  • PHP哈希映射的哈希映射项也使用其他字段来存储信息。并且 - 是的,不仅字符串/整数键很重要,而且还有用于键的字符串本身。
  • 您的案例中包含字符串键的选项将&#34; win&#34;就内存量而言,因为两个选项都将被散列到ulong(无符号长)键散列映射中,因此真正的差异将在值中,其中string-keys选项具有整数(固定长度)值,而整数-keys选项具有字符串(与字符相关的长度)值。但由于可能的碰撞,这可能并非总是如此。
  • &#34;字符串数字&#34;密钥(例如'4')将被视为整数密钥并转换为整数哈希结果,因为它是整数密钥。因此,'4'=>'foo'4 => 'foo'是相同的。

此外,重要提示:此处的图片版权归PHP internals book

PHP数组的哈希映射

PHP数组和C数组

你应该意识到一个非常重要的事情:PHP是用C语言编写的,其中包括&#34;关联数组&#34;根本不存在。所以,在C&#34;数组&#34;正是&#34; array&#34;是 - 即它只是存储器中的一个连续区域,可以通过连续偏移来访问。你的&#34;键&#34;可能只是数字,整数而且只是连续的,从零开始。例如,您不能将3-6'foo'作为&#34;密钥&#34;那里。

为了实现PHP中的数组,有哈希映射选项,它使用哈希函数哈希你的密钥并将它们转换为整数,可用于C阵列。但是,该函数永远无法在字符串键及其整数散列结果之间创建bijection。并且很容易理解为什么:因为设置的cardinality字符串比整数集的基数大得多。让我们举例说明:我们将重新计算最长为10的所有字符串,这些字符串只包含字母数字符号(因此,0-9a-zA-Z,总计62):它可能有62个 10 总串。它在 8.39E + 17 附近。将它与我们对无符号整数(长整数,32位)类型的 4E + 9 进行比较,你会得到这个想法 - 会有碰撞

PHP哈希映射键&amp;碰撞

现在,为了解决冲突,PHP只会将具有相同哈希函数结果的项放入一个链表中。因此,hash-map不仅仅是散列元素的列表&#34;而是它将存储指向元素列表的指针(某些列表中的每个元素将具有相同的散列函数键)。这就是你要指出它将如何影响内存分配的地方:如果你的数组有字符串键,这不会导致冲突,那么这些列表中就不需要额外的指针,因此内存量会减少(实际上,它和#39;开销非常小,但是,由于我们正在谈论精确的内存分配,因此应该考虑到这一点)。并且,同样地,如果您的字符串键将导致许多冲突,那么将创建更多的附加指针,因此总内存量将更多一些。

为了说明这些列表中的关系,这里有一个图形:

  

enter image description here

上面介绍了PHP在应用哈希函数后如何解决冲突。因此,您的一个问题部分就在于此处,指出了碰撞解决方案列表中的指针。此外,链接列表的元素通常称为 buckets ,并且包含指向这些列表头部的指针的数组在内部称为arBuckets。由于结构优化(因此,为了使元素删除,更快),真正的列表元素有两个指针,前一个元素和下一个元素 - 但是这只会在非碰撞/碰撞的存储量方面产生差异数组稍微宽一点,但不会改变概念本身。

还有一个清单:订单

要完全支持PHP中的数组,还需要维护 order ,以便通过另一个内部列表实现。数组的每个元素也是该列表的成员。它在内存分配方面没有什么不同,因为在这两个选项中都应该维护这个列表,但是为了全面了解,我提到了这个列表。这是图形:

  

enter image description here

除了pListLastpListNext之外,还存储了指向订单列表头部和尾部的指针。同样,它与您的问题没有直接关系,但我还会转储内部存储桶结构,这些存在这些指针。

内部的数组元素

现在我们已经准备好了解一下:什么是数组元素,所以,bucket

typedef struct bucket {
    ulong h;
    uint nKeyLength;
    void *pData;
    void *pDataPtr;
    struct bucket *pListNext;
    struct bucket *pListLast;
    struct bucket *pNext;
    struct bucket *pLast;
    char *arKey;
} Bucket;

我们在这里:

  • h是key的整数(ulong)值,它是hash函数的结果。对于整数键,与键本身相同(哈希函数返回自身)
  • pNext / pLast是碰撞解决链接列表中的指针
  • pListNext / pListLast是订单分辨率链接列表中的指针
  • pData是指向存储值的指针。实际上,值与创建数组时插入的值相同,它的 copy ,但为了避免不必要的开销,PHP使用pDataPtr(所以pData = &pDataPtr

从这个角度来看,你可能会得到下一个不同之处:因为字符串键将被哈希(因此,h总是ulong,因此,相同的大小),它将是一个存储在值中的内容。因此,对于您的字符串键数组,将存在整数值,而对于整数键数组,将存在字符串值,这会产生差异。但是 - 不,它不是一个神奇的:你不能节省内存&#34;一直以这种方式存储字符串键,因为如果你的键很大并且会有很多键,它会导致冲突开销(很好,很可能,但当然不能保证)。它会&#34;工作&#34;仅适用于任意短串,不会导致多次碰撞。

哈希表本身

已经讨论了元素(存储桶)及其结构,但也有散列表本身,实际上是数组数据结构。所以,它被称为_hashtable

typedef struct _hashtable {
    uint nTableSize;
    uint nTableMask;
    uint nNumOfElements;
    ulong nNextFreeElement;
    Bucket *pInternalPointer;   /* Used for element traversal */
    Bucket *pListHead;
    Bucket *pListTail;
    Bucket **arBuckets;
    dtor_func_t pDestructor;
    zend_bool persistent;
    unsigned char nApplyCount;
    zend_bool bApplyProtection;
#if ZEND_DEBUG
    int inconsistent;
#endif
} HashTable;

我不会描述所有字段,因为我已经提供了很多信息,这只与问题有关,但我会简要介绍一下这个结构:

  • arBuckets就是上面描述的,桶存储,
  • pListHead / pListTail是订单分辨率列表的指针
  • nTableSize确定哈希表的大小。这与内存分配直接相关:nTableSize总是2的幂。因此,无论你是否在数组中有13个或14个元素:实际大小为16。想要估算数组大小时要考虑到这一点。

结论

很难预测,在你的情况下,一个阵列会比另一个阵列大。是的,有一些指导原则是内部结构,但如果字符串键的长度与整数值相当(例如样本中的'four''one') - 真正的区别在于 - 发生了多少次冲突,分配了多少字节来保存值。

但选择合适的结构应该是感觉问题,而不是记忆。如果您打算构建相应的索引数据,那么选择总是显而易见的。上面的帖子只有一个目标:显示数组如何在PHP中实际工作,以及在哪里可以找到样本中内存分配的差异。

您还可以查看有关数组和文章的文章。 PHP中的哈希表:PHP内部书籍Hash-tables in PHP:我从那里使用了一些图形。另外,要了解如何在PHP中分配值,请查看zval Structure文章,它可以帮助您理解字符串和字符串之间的差异。整数分配数组的值。我没有在这里包含解释,因为对我来说更重要的一点是 - 显示数组数据结构以及在你的问题的字符串键/整数键的上下文中可能有什么不同。

答案 1 :(得分:3)

尽管两个数组都以不同的方式访问(即通过字符串或整数值),但内存模式大致相似。

这是因为字符串分配是作为zval创建的一部分或需要分配新数组键时发生的;不同的是,数字索引不需要整个zval结构,因为它们被存储为(无符号)长。

观察到的内存分配差异很小,很大程度上可归因于memory_get_usage()的不准确性或由于额外的存储桶创建而导致的分配。

结论

如何使用阵列必须成为选择索引方式的指导原则;当你用完它时,内存只会成为这个规则的一个例外。

答案 2 :(得分:3)

从PHP手册垃圾收集http://php.net/manual/en/features.gc.php

gc_enable(); // Enable Garbage Collector
var_dump(gc_enabled()); // true
var_dump(gc_collect_cycles()); // # of elements cleaned up
gc_disable(); // Disable Garbage Collector

PHP不能很好地返回释放的内存;它在线的主要用途并不需要它,有效的垃圾收集需要时间来提供输出;当脚本结束时,无论如何都会返回内存。

垃圾收集发生。

  1. 当你告诉

    int gc_collect_cycles ( void )

  2. 离职时

  3. 脚本结束时
  4. 更好地了解来自网络主机的PHP垃圾收集(无联属关系)。 http://www.sitepoint.com/better-understanding-phps-garbage-collection/

    如果您正在逐字节地考虑如何在内存中设置数据。不同的端口将影响这些值。当数据位于64位字的第一位时,64位CPU的性能最佳。对于最大性能,一个特定的二进制文件,它们将在第一个位上分配一块内存的开始,最多留下7个字节未使用。这个CPU特定的东西取决于编译PHP.exe的编译器。我不能提供任何方法来预测确切的内存使用情况,因为它将由不同的编译器以不同的方式确定。

    Alma Do,post转到发送给编译器的源的细节。 PHP源请求和编译器优化的内容。

    查看您发布的具体示例。当密钥是ascii字母时,它们每个条目占用4个字节(64位)...这对我来说(假设没有垃圾或内存漏洞等),ascii密钥大于64位,但是数字键适合64位字。它向我建议你使用64位计算机,你的PHP.exe是为64位CPU编译的。

答案 3 :(得分:1)

PHP中的数组实现为哈希映射。因此,用于密钥的值的长度对数据要求几乎没有影响。在旧版本的PHP中,大型数组的性能显着下降,因为散列大小在数组创建时得到修复 - 当冲突开始发生然后增加的哈希值数量时,将映射到值的链接列表,然后必须进一步搜索(使用一个O(n)算法)而不是单个值,但最近哈希似乎要么使用更大的默认大小或动态调整大小(它只是工作 - 我真的不能阅读源代码)

从脚本中保存4个字节不会导致Google出现任何不眠之夜。如果您正在编写使用大型数组的代码(节省可能更为重要),那么您可能做错了 - 填充数组所花费的时间和资源可以更好地用于其他地方(例如索引存储)。 / p>