Подтвердить что ты не робот

Почему array_key_exists 1000x медленнее, чем isset на ссылочных массивах?

Я обнаружил, что 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.

Пример Codepad.

4b9b3361

Ответ 1

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

Это блестяще отвечает на такие вопросы.

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

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

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

Глядя на коды операций, мы расскажем вам более полную историю, однако у меня есть предположение.... Большинство встроенных PHP-модулей делают копию массива/объекта и действуют на эту копию (а не копируют- on-write либо, немедленная копия). Наиболее широко известный пример этого - foreach(). Когда вы передаете массив в foreach(), PHP фактически создает копию этого массива и выполняет итерацию на копии. Вот почему вы увидите значительное преимущество в производительности, передав массив в качестве ссылки в foreach следующим образом:

foreach ($ someReallyBigArray как $k = > & $v)

Но это поведение - то, что передается в явной ссылке вроде этого - уникально для foreach(). Поэтому я был бы очень удивлен, если бы он сделал array_key_exists() проверкой быстрее.

Хорошо, обратно к тому, что я получаю..

Большинство встроенных модулей берут копию массива и действуют на эту копию. Я собираюсь начать совершенно безоговорочное предположение, что isset() сильно оптимизирован и что одна из этих оптимизаций, возможно, не делает немедленную копию массива при его передаче.

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

Ответ 2

Вот источник функции 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;
    }

}

Ответ 3

Не array_key_exists, но удаление ссылки (= NULL) вызывает это. Я прокомментировал это из вашего script, и это результат:

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 );