注意,以下答案适用于 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-function 来 hash 你的键并将它们转换为整数,这可以用于 C 数组。但是,该函数将永远无法在字符串键及其整数散列结果之间创建bijection。而且很容易理解为什么:因为字符串集的cardinality 比整数集的基数大得多。让我们用例子来说明:我们将重新计算所有字符串,长度不超过 10,它们只有字母数字符号(所以,0-9、a-z 和 A-Z,总共 62 个):它是 6210 可能的总字符串。大约是 8.39E+17。将它与我们对无符号整数(长整数,32 位)类型的 4E+9 进行比较,您就会明白 - 会有冲突。
PHP 哈希映射键和冲突
现在,为了解决冲突,PHP 只会将具有相同哈希函数结果的项目放入一个链表中。因此,hash-map 不仅仅是“散列元素列表”,而是存储指向元素列表的指针(某个列表中的每个元素都将具有相同的散列函数键)。这就是你指出它将如何影响内存分配的地方:如果你的数组有字符串键,这不会导致冲突,那么这些列表中不需要额外的指针,所以内存量会减少(实际上,它是一个非常小的开销,但是,由于我们正在讨论 精确 内存分配,因此应该考虑到这一点)。而且,同样的,如果你的字符串键会导致很多冲突,那么会创建更多的额外指针,所以总内存量会更多。
为了说明这些列表中的这些关系,这里有一个图形:
上面是 PHP 在应用哈希函数后如何解决冲突的。因此,您的问题之一就在这里,即冲突解决列表中的指针。此外,链表的元素通常称为 buckets,包含指向这些链表头的指针的数组在内部称为 arBuckets。由于结构优化(因此,为了使元素删除等操作更快),真正的列表元素有两个指针,前一个元素和下一个元素 - 但这只会使非冲突/冲突数组的内存量有所不同,但不会改变概念本身。
另一个列表:订单
为了完全支持 PHP 中的数组,还需要维护 order,这可以通过另一个内部列表来实现。数组的每个元素也是该列表的成员。它不会在内存分配方面产生影响,因为在这两个选项中都应该维护这个列表,但是为了全貌,我提到了这个列表。这是图形:
除了pListLast 和pListNext 之外,还存储了指向订单列表头部和尾部的指针。同样,它与您的问题没有直接关系,但我将进一步转储这些指针所在的内部存储桶结构。
内部的数组元素
现在我们准备研究:什么是数组元素,所以,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 文章,它可能会帮助您了解字符串和整数分配数组值的区别。我没有在这里包含解释,因为对我来说更重要的一点是显示数组数据结构以及您的问题的字符串键/整数键的上下文可能有什么不同。