为什么在引用数组上,array_key_exists比isset慢1000倍?

24
我发现,在检查数组引用中是否已设置键时,array_key_existsisset慢1000多倍。 有没有人了解PHP如何实现并解释为什么会出现这种情况?
编辑: 我添加了另一个案例,似乎表明在调用带有参考的函数时需要额外开销。 基准测试示例
function isset_( $key, array $array )
{
    return isset( $array[$key] );
}

$my_array = array();
$start = microtime( TRUE );
for( $i = 1; $i < 10000; $i++ ) {
    array_key_exists( $i, $my_array );
    $my_array[$i] = 0;
}
$stop = microtime( TRUE );
print "array_key_exists( \$my_array ) ".($stop-$start).PHP_EOL;
unset( $my_array, $my_array_ref, $start, $stop, $i );

$my_array = array();
$start = microtime( TRUE );
for( $i = 1; $i < 10000; $i++ ) {
    isset( $my_array[$i] );
    $my_array[$i] = 0;
}
$stop = microtime( TRUE );
print "isset( \$my_array ) ".($stop-$start).PHP_EOL;
unset( $my_array, $my_array_ref, $start, $stop, $i );

$my_array = array();
$start = microtime( TRUE );
for( $i = 1; $i < 10000; $i++ ) {
    isset_( $i, $my_array );
    $my_array[$i] = 0;
}
$stop = microtime( TRUE );
print "isset_( \$my_array ) ".($stop-$start).PHP_EOL;
unset( $my_array, $my_array_ref, $start, $stop, $i );

$my_array = array();
$my_array_ref = &$my_array;
$start = microtime( TRUE );
for( $i = 1; $i < 10000; $i++ ) {
    array_key_exists( $i, $my_array_ref );
    $my_array_ref[$i] = 0;
}
$stop = microtime( TRUE );
print "array_key_exists( \$my_array_ref ) ".($stop-$start).PHP_EOL;
unset( $my_array, $my_array_ref, $start, $stop, $i );

$my_array = array();
$my_array_ref = &$my_array;
$start = microtime( TRUE );
for( $i = 1; $i < 10000; $i++ ) {
    isset( $my_array_ref[$i] );
    $my_array_ref[$i] = 0;
}
$stop = microtime( TRUE );
print "isset( \$my_array_ref ) ".($stop-$start).PHP_EOL;
unset( $my_array, $my_array_ref, $start, $stop, $i );

$my_array = array();
$my_array_ref = &$my_array;
$start = microtime( TRUE );
for( $i = 1; $i < 10000; $i++ ) {
    isset_( $i, $my_array_ref );
    $my_array_ref[$i] = 0;
}
$stop = microtime( TRUE );
print "isset_( \$my_array_ref ) ".($stop-$start).PHP_EOL;
unset( $my_array, $my_array_ref, $start, $stop, $i );

输出

array_key_exists( $my_array ) 0.0056459903717
isset( $my_array ) 0.00234198570251
isset_( $my_array ) 0.00539588928223
array_key_exists( $my_array_ref ) 3.64232587814 // <~ what on earth?
isset( $my_array_ref ) 0.00222992897034
isset_( $my_array_ref ) 4.12856411934 // <~ what on earth?

我使用的是 PHP 5.3.6 版本。

Codepad 示例


1
在互联网上搜索isset vs. array_key_exists,你会发现比你能够阅读的更多资源。 - hakre
2
@hakre 我理解 issetarray_key_exists 的区别(它们的工作原理),我的问题是为什么在引用上使用它们时会出现性能问题。 - Kendall Hopkins
array_key_exists必须检查具体的键值,而isset则不必。如果您必须查找引用的具体值,则还必须额外解析引用。因此,需要更多的工作时间。我无法告诉您为什么需要更多的时间,但您也可以取消引用,这将影响工作量。 - hakre
1
@hakre 我已经更新了我的示例,测试function( $key, $array ) { return isset( $array[$key] ); }。它也很慢,这似乎指向调用带有引用的函数所需的开销。 - Kendall Hopkins
1
我认为你的测试并没有真正展示出你想要的结果。用户空间函数(比如你创建的那个)总是比内置函数慢得多。 - Shane H
4个回答

8
在工作中,我有一个包含名为VLD的PECL扩展的PHP虚拟机实例。这让你可以从命令行执行PHP代码,而不是执行它,它返回生成的opcode。
它非常擅长回答像这样的问题。
如果你想尝试这个方法(如果你对PHP内部工作原理感到好奇的话),你应该在虚拟机上安装它(也就是说,我不会把它安装在我正在开发或部署的机器上)。这是你将用来运行它的命令。 http://pecl.php.net/package/vld
php -d vld.execute=0 -d vld.active=1 -f foo.php

查看操作码将告诉您更完整的故事,但是我有一个猜测...PHP大多数内置功能都会复制数组/对象并对该副本进行操作(不是写时复制,而是立即复制)。最广为人知的例子是foreach()。当您将数组传递给foreach()时,PHP实际上正在复制该数组并在该副本上进行迭代。这就是为什么通过像这样将数组作为引用传递给foreach,您将看到显着的性能提升的原因:
foreach($someReallyBigArray as $k => &$v)
但是这种行为——像那样传入显式引用——是foreach()中独特的。因此,如果它使array_key_exists()检查更快,我会非常惊讶。
好了,回到我的重点...
大多数内置功能都会复制数组并对该副本进行操作。我要冒昧地猜测isset()高度优化,并且其中一种优化可能是在传入时不立即复制数组。
我会尽力回答您可能有的其他问题,但是如果您搜索“zval_struct”(这是PHP内部存储每个变量的数据结构),您可能会阅读很多内容。它是一个C结构体(类似于关联数组),具有诸如“value”、“type”、“refcount”之类的键。

1
我认为这并没有真正回答问题。虽然那个工具看起来非常有趣,我会试一试,但你的回答最多只是猜测。顺便说一下,isset不是一个函数,它是一种语言结构(这就是为什么它可能没有在这里显示开销,直到你将其包装在一个函数中)。 - Kendall Hopkins
2
Kendall,这只是一个“猜测”,因为我没有在php-src树中搜索来回答你的问题。这是一个非常明智的猜测。最近我更多地从事PHP扩展开发(用C语言),而不是PHP代码本身。至于你关于“语言结构”的评论...你是从PHP手册上得到的吗?但你似乎并不理解它的真正含义。所有它意味着的是,在C代码中,它不是作为全局ZEND_FUNCTION实现的。“调用isset()”所需的“开销”与调用“is_a”所需的开销相同,后者是一个ZEND_FUNCTION。 - Shane H
无论如何,相信你想要相信的。你似乎有一个假设,不管事实如何都会坚持。而且你好像多次认为用户空间函数(即在PHP代码中创建的函数)与语言内置函数有些相似。它们唯一共同之处就是它们都被命名为“function”。 - Shane H

3
这里是5.2.17版本array_key_exists函数的源代码。可以看到,即使键为null,PHP也会尝试计算哈希值。不过有趣的是,如果你删除了

标签,代码仍然能够正常工作。
// $my_array_ref[$i] = NULL;

那么它的表现会更好。一定会发生多个哈希查找。

/* {{{ proto bool array_key_exists(mixed key, array search)
   Checks if the given key or index exists in the array */
PHP_FUNCTION(array_key_exists)
{
    zval **key,                 /* key to check for */
         **array;               /* array to check in */

    if (ZEND_NUM_ARGS() != 2 ||
        zend_get_parameters_ex(ZEND_NUM_ARGS(), &key, &array) == FAILURE) {
        WRONG_PARAM_COUNT;
    }

    if (Z_TYPE_PP(array) != IS_ARRAY && Z_TYPE_PP(array) != IS_OBJECT) {
        php_error_docref(NULL TSRMLS_CC, E_WARNING, "The second argument should be either an array or an object");
        RETURN_FALSE;
    }

    switch (Z_TYPE_PP(key)) {
        case IS_STRING:
            if (zend_symtable_exists(HASH_OF(*array), Z_STRVAL_PP(key), Z_STRLEN_PP(key)+1)) {
                RETURN_TRUE;
            }
            RETURN_FALSE;
        case IS_LONG:
            if (zend_hash_index_exists(HASH_OF(*array), Z_LVAL_PP(key))) {
                RETURN_TRUE;
            }
            RETURN_FALSE;
        case IS_NULL:
            if (zend_hash_exists(HASH_OF(*array), "", 1)) {
                RETURN_TRUE;
            }
            RETURN_FALSE;

        default:
            php_error_docref(NULL TSRMLS_CC, E_WARNING, "The first argument should be either a string or an integer");
            RETURN_FALSE;
    }

}

我不认为多几个查找会导致1000倍的速度下降。无论如何,我很确定问题不仅仅是array_key_exists函数。正如您在示例中看到的那样,任何函数都可能会发生这种情况(请参见isset_)。 - Kendall Hopkins
我认为这是zend_hash_index_exists()等函数内部的问题;通常访问未设置的元素比已设置的元素慢。我过去通过首先测试真值(!!$hash->$value),如果仍然不确定,才使用array_key_exists()来优化生产代码,它运行得更快。 - Andras
问题在于数组的分离(也称为复制)… - bwoebi

3
自 PHP 版本 7.4 开始,array_key_exists 的速度与 isset 相当。

请参见https://www.php.net/manual/en/function.array-key-exists.php#125313
以下是一些性能测试结果:
<?php declare(strict_types = 1);

function testPerformance($name, Closure $closure, $runs = 1000000)
{
    $start = microtime(true);
    for (; $runs > 0; $runs--)
    {
        $closure();
    }
    $end = microtime(true);

    printf("Function call %s took %.5f seconds\n", $name, $end - $start);
}

$items = [1111111];
for ($i = 0; $i < 100000; $i++) {
    $items[] = rand(0, 1000000);
}
$items = array_unique($items);
shuffle($items);

$assocItems = array_combine($items, array_fill(0, count($items), true));

$isset = function () use ($assocItems) {
    isset($items[1111111]);
};

$array_key_exists = function () use ($assocItems) {
    array_key_exists(1111111, $assocItems);
};

testPerformance('isset', $isset, 100000);
testPerformance('array_key_exists', $array_key_exists, 100000);

输出:

Function call isset took 0.00561 seconds
Function call array_key_exists took 0.00547 seconds

参考:https://bugs.php.net/bug.php?id=71239 - phils

1
不是array_key_exists的问题,而是移除引用(= NULL)导致了这个问题。我从你的脚本中注释掉它,这就是结果:
array_key_exists( $my_array ) 0.0059430599212646
isset( $my_array ) 0.0027170181274414
array_key_exists( $my_array_ref ) 0.0038740634918213
isset( $my_array_ref ) 0.0025200843811035

只是从array_key_exists( $my_array_ref )部分中删除了取消设置,这是参考的修改部分:

$my_array = array();
$my_array_ref = &$my_array;
$start = microtime( TRUE );
for( $i = 1; $i < 10000; $i++ ) {
    array_key_exists( $i, $my_array_ref );
    // $my_array_ref[$i] = NULL;
}
$stop = microtime( TRUE );
print "array_key_exists( \$my_array_ref ) ".($stop-$start).PHP_EOL;
unset( $my_array, $my_array_ref, $start, $stop, $i );

那么它就没有用了。= NULL 只是放置了一个更大的值。我将在我的示例中将其替换为 0,以使其更清晰。 - Kendall Hopkins
通过删除那行代码,该函数可能会提前返回(因为数组中没有键)。此外,它并不反映我遇到的真实问题(一个带有键的 ref 数组)。 - Kendall Hopkins
2
嗯,可能每次引用发生变化时,哈希都会被触发以重新索引键。 - hakre
嗯,你为什么需要一个引用呢? - hakre
因此,我希望有了解PHP内部的人能够解决这个问题。 - Kendall Hopkins
显示剩余11条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接