【问题标题】:Micro optimization on array keys数组键的微优化
【发布时间】:2017-02-14 15:17:36
【问题描述】:

我有一个数组,我使用其中的一些项来构造更多数组,下面是一个粗略的示例。

$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 的值将用于(并复制)每个数组作为键或内存中的指针被使用,这基本上是我的问题?

【问题讨论】:

  • 我对示例代码感到困惑 - 在循环中您引用 $data['a'],但是在任何行中都没有键 a
  • @ChrisO'Kelly 抱歉,已修复。

标签: php arrays php-internals


【解决方案1】:

虽然我假设内部结构已经有一段时间没有改变了,但this article 表示它们基本上是哈希表,带有一些细微差别以避免键冲突。所以在某种程度上,是的,它确实做到了你所说的。

【讨论】:

  • 我没有逐行阅读链接的文章,但我浏览了它。它说 PHP 中的所有内容都是哈希表,但是当我为 PHP 编写扩展时,有两个不同的函数 array_init()ALLOC_HASHTABLE()zend_hash_init(),将 zval 初始化为数组或哈希表,所以它们显然是不同的。
  • 也许我误解了这个问题,因为它是相当开放的,@FrederickZhang。但是,使用提供的代码,PHP 将通过创建可能的哈希键的 zval 并映射它们(基于链接的文章)来尝试自己优化事物。但是我可能是错的。
  • 我不太确定这个问题本身,但要澄清一下,现在 ZE 并没有使用完全相同的数据结构来处理数组和哈希表。
  • 啊,是的,它不会使用相同的数据结构,我更要指出的是,所做的优化在某种程度上已经在幕后完成了(使用 key => index 的哈希来更快在哈希中搜索值)。
  • 我明白,但我怀疑它是否适用于不同数组的键?假设我有变量 $a,如果我将其用作 不同 数组中的键,则每个数组的 $a 的值将被用作(并复制)作为键,或者内存中的指针将被使用,这基本上是我的问题?
【解决方案2】:

换句话说 - 假设我有变量 $a,如果我将它用作不同数组中的键,则每个数组的 $a 的值将被用作(并复制)作为键,或者将使用内存中的指针,这基本上是我的问题?

以下是 PHP >=5.4 和 PHP 7 之间的区别,这取决于您的环境。我不是 PHP 专家,我的回答可能是错误的,但我已经为 PHP 编写扩展程序已经有一段时间了,我正在尝试根据我的回答你的问题观察。

zend_hash.c,PHP 5.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”的概念是从 PHP 5.4 开始出现的。该字符串将始终在 PHP 5.3 之前和之后被复制。但由于 PHP 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 的 PHP 5.4-5.6 中,如果字符串已经在这个特定进程的内存中,则将使用引用;但是对于 ZTS,它将始终被复制。 (仅供参考,我们在 Linux 中很少需要 ZTS)。

澄清一下,这种情况下的$uniqueKey 字符串不会被实习,因为它是在运行时创建的。实习仅适用于编译时已知(文字)字符串。 @NikiC 感谢您的澄清

PHP 7 怎么样?在zend_hash.c,PHP 7.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 更有效!

简而言之,如果你使用一个已存在的字符串作为哈希表中的键,当然你保持不变,在 PHP

另外,我找到了一篇很棒的文章供你阅读(我稍后也会阅读它,哈哈):http://jpauli.github.io/2015/09/18/php-string-management.html

【讨论】:

  • 这是我在这个网站上这些年来收到的最好的答案。
  • 我喜欢你的“哦,哦,等等,另一个宏,又在哪里?”评论。 Zend 有大量的宏。
  • 澄清一下,这种情况下的$uniqueKey 字符串不会被实习,因为它是在运行时创建的。实习仅适用于编译时已知(文字)字符串。
  • @NikiC 谢谢,我在答案中引用了它:)
猜你喜欢
  • 2011-02-28
  • 2011-07-31
  • 2010-12-05
  • 2014-04-11
  • 1970-01-01
  • 2013-03-24
  • 1970-01-01
  • 2013-09-17
  • 2010-12-29
相关资源
最近更新 更多