数组键的微优化
Micro optimization on array keys
我有一个数组,我正在使用其中的一些项目来构造更多数组,下面是一个粗略的示例。
$rows = [
[1, 2, 3, 'a', 'b', 'c'],
[4, 5, 6, 'd', 'e', 'f'],
[4, 5, 6, 'g', 'h', 'i'],
];
$derivedData = [];
foreach ($rows as $data) {
$key = $data[0] . '-' . $data[1] . '-' . $data[2];
$derivedData['itemName']['count'] ++;
$derivedData['itemName']['items'][$key]['a'] = $data[3];
$derivedData['itemName']['items'][$key]['count'] ++;
}
现在,如果我转储数组,它将看起来像
derivedData: [
itemName: [
count: 3
items: [
1-2-3: [
a: a,
count: 1
],
4-5-6: [
a: g,
count: 2
],
]
]
]
如您所见,derivedData.itemName.count.items
中的键是字符串。如果我改为这样做,我会得到什么好处吗?
$uniqueId = 0;
$uniqueArray = [];
$rows = [
[1, 2, 3, 'a', 'b', 'c'],
[4, 5, 6, 'd', 'e', 'f'],
[4, 5, 6, 'g', 'h', 'i'],
];
$derivedData = [];
foreach ($rows as $data) {
$uniqueArrayKey = $data[0] . '-' . $data[1] . '-' . $data[2];
if (!isset($uniqueArray[$uniqueArrayKey])) {
$uniqueArray[$uniqueArrayKey] = $uniqueId++;
}
$uniqueKey = $uniqueArray[$uniqueArrayKey];
$derivedData['itemName']['count'] ++;
$derivedData['itemName']['items'][$uniqueKey ]['a'] = $data[3];
$derivedData['itemName']['items'][$uniqueKey ]['count'] ++;
}
现在我将有一个索引数组和实际数据数组。
uniqueArray: [
1-2-3: 0,
4-5-6: 1
]
derivedData: [
itemName: [
count: 3
items: [
0: [
a: a,
count: 1
],
1: [
a: g,
count: 2
],
]
]
]
我问自己的问题是 PHP 在使用字符串键时是否会在内部为我执行此操作,即将它们保存在某处并将它们作为键的指针引用而不是每次都复制它们?
换句话说 - 假设我有变量 $a
,如果我将它用作不同数组中的键,那么每个数组都会使用(并复制)$a
的值作为键或者内存中的指针将被使用,这基本上是我的问题?
虽然我假设内部结构有一段时间没有改变,但 this article 表示它们基本上是哈希表,有一些细微差别以避免键冲突。所以在某种程度上,是的,它确实做了你在幕后所说的。
In other words - lets say I have variable $a, if I use that as a key in different arrays would the value of $a be used (and copied) for each array as key or the pointer in memory will be used, that is basically my question?
PHP >=5.4 和 PHP 7 之间的差异,这取决于您的环境。我不是 PHP 专家,我的回答 可能是错误的 但我一直在为 PHP 编程扩展很长一段时间,我正在尝试根据我的观察来回答你的问题。
在zend_hash.c
PHP5.6.26的源码中,我们可以找到这个函数:
ZEND_API int _zend_hash_add_or_update(HashTable *ht, const char *arKey, uint nKeyLength, void *pData, uint nDataSize, void **pDest, int flag ZEND_FILE_LINE_DC)
{
// omitted
if (IS_INTERNED(arKey)) {
p = (Bucket *) pemalloc(sizeof(Bucket), ht->persistent);
p->arKey = arKey;
} else {
p = (Bucket *) pemalloc(sizeof(Bucket) + nKeyLength, ht->persistent);
p->arKey = (const char*)(p + 1);
memcpy((char*)p->arKey, arKey, nKeyLength);
}
// omitted
}
好像是根据IS_INTERNED()
的值来判断是否复制字符串,那么它在哪里呢?首先,在ZendAccelerator.h
中,我们可以找到:
#if ZEND_EXTENSION_API_NO > PHP_5_3_X_API_NO
// omitted
#else
# define IS_INTERNED(s) 0
// omitted
#endif
于是"interned string"的概念从PHP5.4开始出现。该字符串将始终在 PHP 5.3 之前和之中被复制。但是由于 PHP <=5.3 真的已经过时了,我想把它从这个答案中去掉。那么 PHP 5.4-5.6 呢?在 zend_string.h
:
#ifndef ZTS
#define IS_INTERNED(s) \
(((s) >= CG(interned_strings_start)) && ((s) < CG(interned_strings_end)))
#else
#define IS_INTERNED(s) \
(0)
#endif
哦哦,等等,又一个宏,又在哪里?在 zend_globals_macros.h
:
#ifdef ZTS
# define CG(v) TSRMG(compiler_globals_id, zend_compiler_globals *, v)
int zendparse(void *compiler_globals);
#else
# define CG(v) (compiler_globals.v)
extern ZEND_API struct _zend_compiler_globals compiler_globals;
int zendparse(void);
#endif
所以在没有Zend Thread Safety的PHP5.4-5.6中,如果字符串已经在这个特定进程的内存中,就会使用引用;但是对于 ZTS,它将始终被复制。 (仅供参考,我们在 Linux 中很少需要 ZTS)。
To clarify, the $uniqueKey
string in this case will not be interned, because it is created at runtime. Interning only applies to compile-time known (literal) strings.
@NikiC thanks for clarification
PHP7 呢?在zend_hash.c
中,PHP7.0.11的来源,
static zend_always_inline zval *_zend_hash_add_or_update_i(HashTable *ht, zend_string *key, zval *pData, uint32_t flag ZEND_FILE_LINE_DC)
{
zend_ulong h;
uint32_t nIndex;
uint32_t idx;
Bucket *p;
IS_CONSISTENT(ht);
HT_ASSERT(GC_REFCOUNT(ht) == 1);
if (UNEXPECTED(!(ht->u.flags & HASH_FLAG_INITIALIZED))) {
CHECK_INIT(ht, 0);
goto add_to_hash;
} else if (ht->u.flags & HASH_FLAG_PACKED) {
zend_hash_packed_to_hash(ht);
} else if ((flag & HASH_ADD_NEW) == 0) {
p = zend_hash_find_bucket(ht, key);
if (p) {
// omitted
}
}
ZEND_HASH_IF_FULL_DO_RESIZE(ht); /* If the Hash table is full, resize it */
add_to_hash:
HANDLE_BLOCK_INTERRUPTIONS();
idx = ht->nNumUsed++;
ht->nNumOfElements++;
if (ht->nInternalPointer == HT_INVALID_IDX) {
ht->nInternalPointer = idx;
}
zend_hash_iterators_update(ht, HT_INVALID_IDX, idx);
p = ht->arData + idx;
p->key = key;
if (!ZSTR_IS_INTERNED(key)) {
zend_string_addref(key);
ht->u.flags &= ~HASH_FLAG_STATIC_KEYS;
zend_string_hash_val(key);
}
// omitted
}
ZEND_API zval* ZEND_FASTCALL _zend_hash_str_add(HashTable *ht, const char *str, size_t len, zval *pData ZEND_FILE_LINE_DC)
{
zend_string *key = zend_string_init(str, len, ht->u.flags & HASH_FLAG_PERSISTENT);
zval *ret = _zend_hash_add_or_update_i(ht, key, pData, HASH_ADD ZEND_FILE_LINE_RELAY_CC);
zend_string_release(key);
return ret;
}
仅供参考,
#define ZSTR_IS_INTERNED(s) (GC_FLAGS(s) & IS_STR_INTERNED)
哇,所以 PHP 7 实际上引入了一个新的、惊人的 zend_string
结构,它与 RC 和垃圾收集一起工作!这比 PHP 5.6!
中的要有效得多
简而言之,如果你使用一个已经存在的字符串作为散列table中的键,当然你保持不变,在PHP <=5.3中,很可能被复制;在 PHP 5.4 中没有 ZTS,已引用;在带有 ZTS 的 PHP 5.4 中,已复制;在 PHP 7 中,引用。
此外,我找到了一篇很棒的文章供您阅读(我稍后也会阅读,哈哈):http://jpauli.github.io/2015/09/18/php-string-management.html
我有一个数组,我正在使用其中的一些项目来构造更多数组,下面是一个粗略的示例。
$rows = [
[1, 2, 3, 'a', 'b', 'c'],
[4, 5, 6, 'd', 'e', 'f'],
[4, 5, 6, 'g', 'h', 'i'],
];
$derivedData = [];
foreach ($rows as $data) {
$key = $data[0] . '-' . $data[1] . '-' . $data[2];
$derivedData['itemName']['count'] ++;
$derivedData['itemName']['items'][$key]['a'] = $data[3];
$derivedData['itemName']['items'][$key]['count'] ++;
}
现在,如果我转储数组,它将看起来像
derivedData: [
itemName: [
count: 3
items: [
1-2-3: [
a: a,
count: 1
],
4-5-6: [
a: g,
count: 2
],
]
]
]
如您所见,derivedData.itemName.count.items
中的键是字符串。如果我改为这样做,我会得到什么好处吗?
$uniqueId = 0;
$uniqueArray = [];
$rows = [
[1, 2, 3, 'a', 'b', 'c'],
[4, 5, 6, 'd', 'e', 'f'],
[4, 5, 6, 'g', 'h', 'i'],
];
$derivedData = [];
foreach ($rows as $data) {
$uniqueArrayKey = $data[0] . '-' . $data[1] . '-' . $data[2];
if (!isset($uniqueArray[$uniqueArrayKey])) {
$uniqueArray[$uniqueArrayKey] = $uniqueId++;
}
$uniqueKey = $uniqueArray[$uniqueArrayKey];
$derivedData['itemName']['count'] ++;
$derivedData['itemName']['items'][$uniqueKey ]['a'] = $data[3];
$derivedData['itemName']['items'][$uniqueKey ]['count'] ++;
}
现在我将有一个索引数组和实际数据数组。
uniqueArray: [
1-2-3: 0,
4-5-6: 1
]
derivedData: [
itemName: [
count: 3
items: [
0: [
a: a,
count: 1
],
1: [
a: g,
count: 2
],
]
]
]
我问自己的问题是 PHP 在使用字符串键时是否会在内部为我执行此操作,即将它们保存在某处并将它们作为键的指针引用而不是每次都复制它们?
换句话说 - 假设我有变量 $a
,如果我将它用作不同数组中的键,那么每个数组都会使用(并复制)$a
的值作为键或者内存中的指针将被使用,这基本上是我的问题?
虽然我假设内部结构有一段时间没有改变,但 this article 表示它们基本上是哈希表,有一些细微差别以避免键冲突。所以在某种程度上,是的,它确实做了你在幕后所说的。
In other words - lets say I have variable $a, if I use that as a key in different arrays would the value of $a be used (and copied) for each array as key or the pointer in memory will be used, that is basically my question?
PHP >=5.4 和 PHP 7 之间的差异,这取决于您的环境。我不是 PHP 专家,我的回答 可能是错误的 但我一直在为 PHP 编程扩展很长一段时间,我正在尝试根据我的观察来回答你的问题。
在zend_hash.c
PHP5.6.26的源码中,我们可以找到这个函数:
ZEND_API int _zend_hash_add_or_update(HashTable *ht, const char *arKey, uint nKeyLength, void *pData, uint nDataSize, void **pDest, int flag ZEND_FILE_LINE_DC)
{
// omitted
if (IS_INTERNED(arKey)) {
p = (Bucket *) pemalloc(sizeof(Bucket), ht->persistent);
p->arKey = arKey;
} else {
p = (Bucket *) pemalloc(sizeof(Bucket) + nKeyLength, ht->persistent);
p->arKey = (const char*)(p + 1);
memcpy((char*)p->arKey, arKey, nKeyLength);
}
// omitted
}
好像是根据IS_INTERNED()
的值来判断是否复制字符串,那么它在哪里呢?首先,在ZendAccelerator.h
中,我们可以找到:
#if ZEND_EXTENSION_API_NO > PHP_5_3_X_API_NO
// omitted
#else
# define IS_INTERNED(s) 0
// omitted
#endif
于是"interned string"的概念从PHP5.4开始出现。该字符串将始终在 PHP 5.3 之前和之中被复制。但是由于 PHP <=5.3 真的已经过时了,我想把它从这个答案中去掉。那么 PHP 5.4-5.6 呢?在 zend_string.h
:
#ifndef ZTS
#define IS_INTERNED(s) \
(((s) >= CG(interned_strings_start)) && ((s) < CG(interned_strings_end)))
#else
#define IS_INTERNED(s) \
(0)
#endif
哦哦,等等,又一个宏,又在哪里?在 zend_globals_macros.h
:
#ifdef ZTS
# define CG(v) TSRMG(compiler_globals_id, zend_compiler_globals *, v)
int zendparse(void *compiler_globals);
#else
# define CG(v) (compiler_globals.v)
extern ZEND_API struct _zend_compiler_globals compiler_globals;
int zendparse(void);
#endif
所以在没有Zend Thread Safety的PHP5.4-5.6中,如果字符串已经在这个特定进程的内存中,就会使用引用;但是对于 ZTS,它将始终被复制。 (仅供参考,我们在 Linux 中很少需要 ZTS)。
To clarify, the
$uniqueKey
string in this case will not be interned, because it is created at runtime. Interning only applies to compile-time known (literal) strings. @NikiC thanks for clarification
PHP7 呢?在zend_hash.c
中,PHP7.0.11的来源,
static zend_always_inline zval *_zend_hash_add_or_update_i(HashTable *ht, zend_string *key, zval *pData, uint32_t flag ZEND_FILE_LINE_DC)
{
zend_ulong h;
uint32_t nIndex;
uint32_t idx;
Bucket *p;
IS_CONSISTENT(ht);
HT_ASSERT(GC_REFCOUNT(ht) == 1);
if (UNEXPECTED(!(ht->u.flags & HASH_FLAG_INITIALIZED))) {
CHECK_INIT(ht, 0);
goto add_to_hash;
} else if (ht->u.flags & HASH_FLAG_PACKED) {
zend_hash_packed_to_hash(ht);
} else if ((flag & HASH_ADD_NEW) == 0) {
p = zend_hash_find_bucket(ht, key);
if (p) {
// omitted
}
}
ZEND_HASH_IF_FULL_DO_RESIZE(ht); /* If the Hash table is full, resize it */
add_to_hash:
HANDLE_BLOCK_INTERRUPTIONS();
idx = ht->nNumUsed++;
ht->nNumOfElements++;
if (ht->nInternalPointer == HT_INVALID_IDX) {
ht->nInternalPointer = idx;
}
zend_hash_iterators_update(ht, HT_INVALID_IDX, idx);
p = ht->arData + idx;
p->key = key;
if (!ZSTR_IS_INTERNED(key)) {
zend_string_addref(key);
ht->u.flags &= ~HASH_FLAG_STATIC_KEYS;
zend_string_hash_val(key);
}
// omitted
}
ZEND_API zval* ZEND_FASTCALL _zend_hash_str_add(HashTable *ht, const char *str, size_t len, zval *pData ZEND_FILE_LINE_DC)
{
zend_string *key = zend_string_init(str, len, ht->u.flags & HASH_FLAG_PERSISTENT);
zval *ret = _zend_hash_add_or_update_i(ht, key, pData, HASH_ADD ZEND_FILE_LINE_RELAY_CC);
zend_string_release(key);
return ret;
}
仅供参考,
#define ZSTR_IS_INTERNED(s) (GC_FLAGS(s) & IS_STR_INTERNED)
哇,所以 PHP 7 实际上引入了一个新的、惊人的 zend_string
结构,它与 RC 和垃圾收集一起工作!这比 PHP 5.6!
简而言之,如果你使用一个已经存在的字符串作为散列table中的键,当然你保持不变,在PHP <=5.3中,很可能被复制;在 PHP 5.4 中没有 ZTS,已引用;在带有 ZTS 的 PHP 5.4 中,已复制;在 PHP 7 中,引用。
此外,我找到了一篇很棒的文章供您阅读(我稍后也会阅读,哈哈):http://jpauli.github.io/2015/09/18/php-string-management.html