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

我发现它比检查时慢1000倍以上,如果在数组引用中设置了键。有没有人了解PHP是如何实现的,解释为什么这是真的?array_key_existsisset

编辑:我添加了另一个案例,似乎表明在使用引用调用函数时需要开销。

基准测试示例

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。

密码键盘示例


答案 1

在工作中,我有一个PHP的VM实例,其中包含一个名为VLD的PECL扩展。这允许您从命令行执行PHP代码,而不是执行它,而是返回生成的操作码。

它在回答这样的问题方面非常出色。

http://pecl.php.net/package/vld

万一你走这条路(如果你通常对PHP内部的工作方式感到好奇,我认为你应该)你绝对应该把它安装在虚拟机上(也就是说,我不会把它安装在我试图开发或部署的机器上)。这是你用来让它唱歌的命令:

php -d vld.execute=0 -d vld.active=1 -f foo.php

查看操作码会告诉你一个更完整的故事,但是,我有一个猜测....大多数PHP的内置功能都会制作数组/对象的副本,并针对该副本执行操作(而不是写入时复制,而是即时副本)。最广为人知的例子是foreach()。当你将一个数组传递到foreach()中时,PHP实际上是在制作该数组的副本并迭代副本。Whis 就是为什么通过将数组作为引用传递到 foreach 中,您会看到显著的性能优势,如下所示:

foreach($someReallyBigArray as $k => &$v)

但是这种行为 - 传入这样的显式引用 - 是foreach()所独有的。因此,如果它更快地进行array_key_exists()检查,我会感到非常惊讶。

好吧,回到我所得到的..

大多数内置组件获取数组的副本并对其副本执行操作。我将冒昧地猜测 isset() 是高度优化的,其中一个优化可能是在传入数组时不要立即复制数组。

我会尝试回答你可能有的任何其他问题,但你可能会读到很多谷歌的“zval_struct”(这是PHP内部存储每个变量的数据结构。它是一个C结构(想想......一个关联数组),具有诸如“value”,“type”,“refcount”之类的键。


答案 2

下面是 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;
    }

}

推荐