Почему array_key_exists в 1000 раз медленнее, чем isset для ссылочных массивов? - PullRequest
22 голосов
/ 14 июня 2011

Я обнаружил, что array_key_exists более чем в 1000 раз медленнее, чем isset при проверке, установлен ли ключ в ссылке на массив. Кто-нибудь, кто понимает, как реализован PHP, объясняет, почему это так?

EDIT: Я добавил еще один случай, который, по-видимому, указывает на то, что при вызове функций со ссылкой требуются накладные расходы.

Пример теста

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.

Пример кодовой панели .

Ответы [ 3 ]

8 голосов
/ 18 июня 2011

На работе у меня есть экземпляр виртуальной машины PHP, который включает расширение PECL под названием VLD. Это позволяет выполнять код PHP из командной строки и вместо того, чтобы выполнять его, вместо этого возвращает сгенерированный код операции.

Замечательно отвечать на такие вопросы.

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

На всякий случай, если вы пойдете по этому пути (и если вам, как правило, любопытно, как PHP работает внутри, я думаю, вам следует), вам определенно следует установить его на виртуальной машине (то есть я бы не стал устанавливать его на машина, которую я пытаюсь развить или развернуть). И эту команду вы будете использовать, чтобы заставить ее петь:

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 () высоко оптимизирован и что одна из этих оптимизаций, возможно, состоит в том, чтобы не делать немедленной копии массива при его передаче.

Я постараюсь ответить на любые другие вопросы, которые у вас могут возникнуть, но вы, вероятно, могли бы прочитать многие из вас в Google, чтобы найти "zval_struct" (которая представляет собой структуру данных во внутреннем устройстве PHP, которая хранит каждую переменную. Это структура C (подумайте .. ассоциативный массив), который имеет ключи типа "value", "type", "refcount".

3 голосов
/ 01 июля 2011

Вот источник функции array_key_exists для 5.2.17. Вы можете видеть, что даже если ключ имеет значение 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;
    }

}
1 голос
/ 14 июня 2011

Не 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 );
...