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

Geo*_*ili 22 php arrays memory-management php-internals

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

假设我们有:

$array = array
(
    'one'   => 1,
    'two'   => 2,
    'three' => 3,
    'four'  => 4,
);
Run Code Online (Sandbox Code Playgroud)

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

$array = array
(
    1 => 'one',
    2 => 'two',
    3 => 'three',
    4 => 'four',
);
Run Code Online (Sandbox Code Playgroud)

需要1136个字节

据我所知,keyvalue肯定都会有不同的存储机制,但请你其实可以点我的原则,它是如何工作的?

示例2 (对于@teuneboon):

$array = array
(
    'one'   => '1',
    'two'   => '2',
    'three' => '3',
    'four'  => '4',
);
Run Code Online (Sandbox Code Playgroud)

1168个字节

$array = array
(
    '1' => 'one',
    '2' => 'two',
    '3' => 'three',
    '4' => 'four',
);
Run Code Online (Sandbox Code Playgroud)

1136个字节

消耗相同的内存:

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

Alm*_* Do 25

注意,下面的答案适用于版本7 之前的PHP ,因为在PHP 7中引入了主要的更改,这些更改也涉及值结构.

TL; DR

你的问题实际上并不是关于"PHP中的内存如何工作的"(这里,我认为,你的意思是"内存分配"),而是关于"数组如何在PHP中工作" - 而这两个问题是不同的.总结一下下面的内容:

  • PHP数组在经典意义上不是"数组".它们是哈希映射
  • PHP数组的哈希映射具有特定的结构,并使用许多其他存储内容,例如内部链接指针
  • PHP哈希映射的哈希映射项也使用其他字段来存储信息.并且 - 是的,不仅字符串/整数键很重要,而且还有用于键的字符串本身.
  • 在你的情况下使用字符串键的选项将在内存量方面"赢",因为两个选项将被散列到ulong(无符号长)键散列映射中,因此真正的差异将在值中,其中string-keys选项具有整数(固定 - length)值,而integer-keys选项具有字符串(chars-dependent length)值.但由于可能的碰撞,这可能并非总是如此.
  • "字符串数字"键,例如'4',将被视为整数键并转换为整数哈希结果,因为它是整数键.因此,'4'=>'foo'4 => 'foo'相同的事情.

另外,重要提示:此处的图形是PHP内部书籍的版权

PHP数组的哈希映射

PHP数组和C数组

你应该意识到一个非常重要的事情:PHP是用C语言编写的,其中诸如"关联数组"之类的东西根本不存在.因此,在C"数组"中正好是"数组" - 即它只是存储器中的连续区域,可以通过连续的偏移来访问.您的"键"可能只是数字,整数,只能是连续的,从零开始.你不能拥有,例如3,-6,'foo'为你的"钥匙"在那里.

因此,为了实现PHP中的数组,有散列映射选项,它使用散列函数散列键并将它们转换为整数,这些整数可用于C数组.但是,该函数永远无法在字符串键及其整数散列结果之间创建双射.这很容易理解为什么:因为基数设置字符串的多,更大的是整数集的基数.让我们来举例说明与示例:我们将讲述所有的字符串,最大长度是10,其中只有字母数字符号(所以0-9,a-z并且A-Z,共有62个):这是62个10总字符串可能.它大约是8.39E + 17.将它与我们对无符号整数(长整数,32位)类型的4E + 9进行比较,你会得到这个想法 - 会有碰撞.

PHP哈希映射键和冲突

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

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

在此输入图像描述

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

还有一个清单:订单

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

在此输入图像描述

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

内部的数组元素

现在我们准备调查:什么是数组元素,所以,:

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;
Run Code Online (Sandbox Code Playgroud)

我们到了:

  • h是key的整数(ulong)值,它是hash函数的结果.对于整数键,它与键本身相同(哈希函数返回自身)
  • pNext/ pLast是冲突解决链接列表中的指针
  • pListNext/ pListLast是订单分辨率链表中的指针
  • pData是指向存储值的指针.实际上,值与在数组创建时插入的值不同,它是副本,但为了避免不必要的开销,PHP使用pDataPtr(so 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;
Run Code Online (Sandbox Code Playgroud)

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

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

结论

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

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

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