【问题标题】:How does PHP memory actually workPHP 内存实际上是如何工作的
【发布时间】:2014-09-24 17:21:02
【问题描述】:

我一直听到并搜索新的 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',

【问题讨论】:

  • 使用注释因为这更像是一个假设而不是实际答案:我认为这是因为当您使用整数作为索引 PHP“假设”数组只是一个列表,因此将其保存为这:$array =(空,'一','二','三','四')。所以它不必存储实际的整数 1,2,3 和 4。
  • 如果您对数组特别感兴趣,请阅读 PHP 核心开发人员之一的this blog post
  • 我的假设是键是散列的(因为 PHP 数组是散列映射),所以它们的长度是无关紧要的。第二个示例只是具有更大的值,这会占用更多的内存。
  • @deceze 我假设 PHP 不会只存储键的哈希值 ;-)
  • @zerkms 当然,我没有向函数传递任何东西,所以默认情况下它是false

标签: php arrays memory-management php-internals


【解决方案1】:

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

TL;DR

您的问题实际上不是关于“PHP 中的内存是如何工作的”(在这里,我假设您的意思是“内存分配”),而是关于“数组在 PHP 中的工作方式” - 这两个问题是不同的。总结一下下面写的:

  • PHP 数组不是传统意义上的“数组”。它们是哈希映射
  • PHP 数组的 Hash-map 具有特定的结构并使用许多额外的存储内容,例如内部链接指针
  • PHP 哈希映射的哈希映射项也使用附加字段来存储信息。而且 - 是的,不仅字符串/整数键很重要,而且字符串本身也很重要,它们用于您的键。
  • 在您的情况下,带有字符串键的选项将在内存量方面“获胜”,因为这两个选项都将被散列到 ulong(无符号长)键哈希映射中,因此真正的区别在于值,其中字符串键选项具有整数(固定长度)值,而整数键选项具有字符串(与字符相关的长度)值。但由于可能发生冲突,这可能并不总是正确的。
  • “字符串-数字”键,例如'4',将被视为整数键并转换为整数哈希结果,因为它是整数键。因此,'4'=>'foo'4 => 'foo' 是同一个东西。

另外,重要提示:这里的图片版权归PHP internals book

PHP 数组的哈希映射

PHP 数组和 C 数组

您应该意识到一件非常重要的事情:PHP 是用 C 语言编写的,其中根本不存在“关联数组”之类的东西。因此,在 C 中,“数组”正是“数组”的含义——即它只是内存中的一个连续区域,可以通过 consecutive 偏移量访问。您的“键”可能只有数字、整数和连续的,从零开始。例如,您不能将3,-6,'foo' 作为您的“键”。

所以要实现数组,在 PHP 中,有 hash-map 选项,它使用 hash-functionhash 你的键并将它们转换为整数,这可以用于 C 数组。但是,该函数将永远无法在字符串键及其整数散列结果之间创建bijection。而且很容易理解为什么:因为字符串集的cardinality 比整数集的基数大得多。让我们用例子来说明:我们将重新计算所有字符串,长度不超过 10,它们只有字母数字符号(所以,0-9a-zA-Z,总共 62 个):它是 6210 可能的总字符串。大约是 8.39E+17。将它与我们对无符号整数(长整数,32 位)类型的 4E+9 进行比较,您就会明白 - 会有冲突

PHP 哈希映射键和冲突

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

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

上面是 PHP 在应用哈希函数后如何解决冲突的。因此,您的问题之一就在这里,即冲突解决列表中的指针。此外,链表的元素通常称为 buckets,包含指向这些链表头的指针的数组在内部称为 arBuckets。由于结构优化(因此,为了使元素删除等操作更快),真正的列表元素有两个指针,前一个元素和下一个元素 - 但这只会使非冲突/冲突数组的内存量有所不同,但不会改变概念本身。

另一个列表:订单

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

除了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 是键的整数(ulong)值,它是哈希函数的结果。对于整数键,它与键本身相同(哈希函数返回自身)
  • pNext / pLast 是冲突解决链表中的指针
  • pListNext/pListLast是订单解析链表中的指针
  • pData 是指向存储值的指针。实际上,值与创建数组时插入的值不同,它是 copy,但是为了避免不必要的开销,PHP 使用了pDataPtr(所以pData = &pDataPtr

从这个角度来看,您可能会得到下一个区别:由于字符串键将被散列(因此,h 始终是ulong,因此大小相同),这将是什么的问题存储在值中。因此,对于您的字符串键数组将有整数值,而对于整数键数组将有字符串值,这会有所不同。但是 - 不,这不是魔法:你不能一直以这种方式存储字符串键来“节省内存”,因为如果你的键很大并且会有很多,它将导致冲突开销(嗯,概率非常高,但当然不能保证)。它只会对任意短字符串“起作用”,不会导致很多冲突。

哈希表本身

关于元素(桶)和它们的结构已经谈过了,但还有哈希表本身,实际上就是数组数据结构。所以,它被称为_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 文章,它可能会帮助您了解字符串和整数分配数组值的区别。我没有在这里包含解释,因为对我来说更重要的一点是显示数组数据结构以及您的问题的字符串键/整数键的上下文可能有什么不同。

【讨论】:

  • 非常感谢您的详细解答和您的宝贵时间
【解决方案2】:

虽然两个数组的访问方式不同(即通过字符串或整数值),但内存模式大多相似。

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

观察到的内存分配差异非常小,主要归因于 memory_get_usage() 的不准确或由于创建了额外的存储桶而导致的分配。

结论

你想如何使用你的数组必须是选择如何索引的指导原则;只有当你用完内存时,内存才应该成为这条规则的例外。

【讨论】:

    【解决方案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. 脚本结束时

    通过网络主机更好地了解 PHP 的垃圾收集(无从属关系)。 http://www.sitepoint.com/better-understanding-phps-garbage-collection/

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

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

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

    【讨论】:

    • 我理解这个主题,但我不相信这个“​​不准确”导致我问这个问题
    • @GeorgeGarchagudashvili 如果您正在逐字节考虑如何在内存中设置数据。不同的端口会影响这些值。当数据位于 64 位字的第一位时,64 位 CPU 的性能最佳。为了最大限度地提高性能,特定二进制文件将在第一位分配内存块的开头,最多留出 7 个字节未使用。这个特定于 CPU 的东西取决于用于编译 PHP.exe 的编译器。我无法提供任何方法来预测确切的内存使用情况,因为不同的编译器会以不同的方式确定它。
    • @GeorgeGarchagudashvili 查看您发布的具体示例。当密钥是 ascii 字母时,它们每个条目多占用 4 个字节(64 位)......这向我表明,假设没有垃圾或内存漏洞,ascii 密钥大于 64 位,但数字键是合适的在 64 位字中。它建议我使用 64 位计算机,并且您的 PHP.exe 是为 64 位 CPU 编译的。
    • 谢谢,你说得对。您可以在答案中包含您的最后评论吗?我会给你我的赏金,因为你抓住了我陷入困境的案例,但接受@Alma Do's 作为答案,因为它确实处理 PHP 内存如何实际工作?
    • 对不起,我有点困惑,我很感兴趣 key => value 是如何应用于 php 内存的(第一个示例),其余示例我在我的问题,并且以某种方式使我摆脱了主要问题。非常感谢,非常感谢
    【解决方案4】:

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

    从您的脚本中节省 4 个字节不会让 Google 彻夜难眠。如果您正在编写使用大型数组的代码(其中节省可能更显着),您可能做错了 - 填充数组所花费的时间和资源最好花在其他地方(如索引存储)。

    【讨论】:

    • 如果您正在使用大型枚举数组,从 0 开始的顺序键,并且提前知道它们将有多大,那么 SPLFixedArray 是一个主要的内存节省
    • hashmap 的使用如何降低键值的内存使用率?
    • 散列映射的意义有多个键映射到同一个散列上——每个散列只有一个键是没有意义的
    • @fast:不。哈希映射的目的是在查找和插入/删除中获取 log(n) 的顺序。请参阅en.wikipedia.org/wiki/Hash_table 特别注意图中下半部分。
    • @symcbean 感谢您的回答,这里的重点不是在内存中保存一些字节,而是要知道它是如何处理这些事情的......
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-11-03
    • 2021-01-21
    • 2011-09-27
    • 2021-12-16
    • 2013-03-14
    • 2021-03-23
    相关资源
    最近更新 更多