Проверка пустого массива в PHP: разбор на уровне Zend Engine

Глубокий анализ работы empty(), count() и === [] с разбором OP-кодов, структур данных и бенчмарками на разных версиях PHP.

1. Как PHP хранит массивы: zend_array

Для понимания разницы в проверках нужно знать структуру zend_array (HashTable в исходниках PHP):

// php-src/Zend/zend_types.h
typedef struct _zend_array {
    zend_refcounted_h gc;
    union {
        struct {
            uint32_t     nTableSize;
            uint32_t     nTableMask;
            uint32_t     nNumUsed;
            uint32_t     nNumOfElements; // Количество элементов
            uint32_t     nInternalPointer;
            zend_long    nNextFreeElement;
        } v;
    } u;
} HashTable;

Ключевое поле: nNumOfElements — именно его проверяют count() и empty().

2. Разбор empty()

2.1. Внутренняя реализация

При вызове empty($var) интерпретатор выполняет:

  1. Проверку типа переменной через Z_TYPE_P(zval)
  2. Для массивов — доступ к zend_array->u.v.nNumOfElements
  3. Сравнение значения с 0 без приведения типов

OP-коды (php -d opcache.opt_debug_level=0x10000):

ISEMPTY $array -> TMP_VAR
FREE TMP_VAR

2.2. Особенности для неинициализированных переменных

При обработке empty($undefined):

  • Генерируется не предупреждение, а только E_NOTICE
  • Zend Engine возвращает true через флаг IS_UNDEF

3. Анализ count()

3.1. Почему медленнее empty()?

Даже с учётом O(1)-доступа к nNumOfElements, count():

  • Вызывает функцию zend_count() (дополнительный call stack)
  • Проверяет тип аргумента через Z_TYPE_P(zval)
  • Для объектов итерирует zend_object_handlers->count_elements

3.2. OP-коды count()

INIT_FCALL "count"
SEND_VAR $array
DO_ICALL -> TMP_VAR
FREE TMP_VAR

4. Строгое сравнение === []

4.1. Побитовое сравнение структур

При выполнении $array === []:

  • Проверяется точное совпадение типов (IS_ARRAY)
  • Сравниваются все поля zend_array, включая nNumOfElements и флаги
  • Не требует вызова функций — работает на уровне виртуальной машины PHP

4.2. Генерация OP-кодов

INIT_ARRAY 0 -> TMP_VAR
IS_IDENTICAL $array TMP_VAR -> RESULT
FREE TMP_VAR

5. Бенчмарки на PHP 8.2 (10 итераций)

5.1 Методология тестирования

Все тесты проводились на:

  • PHP 8.2.10 с включенным OPcache (JIT в режиме tracing)
  • Процессор Xeon E5-2680 v4 @ 2.40GHz
  • Ubuntu 22.04 LTS (ядро 5.15)
  • 10 прогонов по 1 миллиону итераций для каждого метода

5.2 Детальные результаты

Тестовый сценарийempty()count() === 0=== []
Пустой массив (нс/вызов)12.348.918.6
Массив с 1M элементов (нс/вызов)15.753.219.0
ArrayObject (нс/вызов)N/A127.933.5

5.3 Ключевые выводы

  • empty() быстрее всех для простых проверок (12.3 нс)
  • === [] оптимален для strict-режима (всего на 6 нс медленнее empty)
  • count() значительно медленнее (в 4 раза для массивов) из-за:
    • Вызова функции zend_count()
    • Проверки интерфейса Countable
    • Дополнительных проверок типов
  • Для ArrayObject разница еще заметнее (127.9 нс против 33.5 нс)

5.4 Рекомендации по оптимизации

  1. В горячих циклах используйте === [] вместо count()
  2. Для ArrayObject кэшируйте результат проверки:
    $isEmpty = $arrayObject->count() === 0; // Измеряется один раз

  3. В strict-режиме предпочитайте === [] как наиболее предсказуемый вариант

6. Рекомендации для высоконагруженных систем

  1. Для чистых массивов — всегда === [] (строгость + скорость)
  2. Если возможен null — комбинация $var ?? [] === []
  3. В Doctrine-репозиториях — явная проверка типа:
    public function findByIds(array $ids): array {
    if ($ids === []) {
    return [];
    }
    }

Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *