PHP Internals

Zval

Структура данных zval(Zend value) используется для представления любых значений PHP. Zval хранит в себе само значение и тип этого значения. Это необходимо потому что PHP — это язык с динамической типизацией и поэтому тип переменных известен только во время выполнения программы (run-time), а не во время компиляции (compile-time). Кроме того, тип переменной может быть изменен в течение жизни zval, то есть zval ранее хранимый как целое число (integer) позднее может содержать строку (string).

Структура zval в PHP5 выглядит так:

typedef struct _zval_struct {
    zvalue_value value;
    zend_uint refcount__gc;
    zend_uchar type;
    zend_uchar is_ref__gc;
} zval;

Тип переменной хранится как целочисленная метка (type tag, unsigned char). Метка может принимать одно из 8 значений, которое соответствует 8 типам данных доступных в PHP. Эти значения должны присваиваться с использованием констант вида IS_TYPE. Например, IS_NULLсоответствует типу данных null, а IS_STRING — строке.

zvalue_value — это union. Union — в языке С – это структура, в которой можно объявить несколько членов разных типов, но лишь один компонент может быть доступен для использования в данный момент времени, размер union равен размеру самого большого компонента. Все компоненты объединения хранятся в памяти в одном месте и могут интерпретироваться по-разному, в зависимости от того, к кому из них вы обращаетесь. Если считать lval, то его значение будет интерпретировано как знаковое целочисленное. Значение dval будет представлено в виде числа двойной точности с плавающей запятой. И так далее. Чтобы узнать, какой компонент объединения используется в данный момент, можно посмотреть текущее значение свойства type.

typedef union _zvalue_value {
    long lval;                 // Для булевых, целочисленных и ресурсов
    double dval;               // Для чисел с плавающей запятой
    struct {                   // Для строковых
        char *val;
        int len;
    } str;
    HashTable *ht;             // Для массивов
    zend_object_value obj;     // Для объектов
    zend_ast *ast;             // Для констант
} zvalue_value;

refcount__gc - когда вы присваиваете переменной значение другой переменной, то они обе ссылаются на один zval, а refcount инкрементируется. Теперь, если вы захотите изменить значение одной из этих переменных, то PHP, увидя refcount больше 1, скопирует этот zval, сделает изменения там, и ваша переменная будет указывать уже на новый zval. Эта техника называется copy on write, и она позволяет неплохо снизить потребление памяти.

У подсчёта ссылок есть один серьёзный недостаток: этот механизм не способен определять циклические ссылки. Для этого в PHP используется дополнительный инструмент — циклический сборщик мусора. Каждый раз, когда значение refcount уменьшается и возникает вероятность, что zvalстал частью цикла, он записывается в root buffer. Когда этот буфер заполняется, потенциальные циклы помечаются и зачищаются сборщиком мусора.

А что происходит со ссылками(пхпшными)? Все очень просто: если вы создаете ссылку от переменной, то флаг is_ref становится равным 1, и больше вышеописанная оптимизация для этого zval-а применяться не будет.

Вот список основных проблем, связанных с реализацией zval в PHP 5:

  • Zval (почти) всегда требуется размещать в куче.
  • Zval всегда требуют использования подсчёта ссылок и сбора информации о циклах. Даже в тех случаях, когда расшаривание значений не стоит потраченных ресурсов (целочисленные) или циклы не могут возникнуть в принципе.
  • Прямой подсчёт ссылок приводит к двойному выполнению этой процедуры в случае с объектами и ресурсами.
  • В некоторых случаях приходится прибегать к большому количеству обходных манёвров. Например, чтобы получить доступ к объекту, хранящемуся в переменной, необходимо суммарно разыменовать четыре указателя, со всеми сопутствующими цепочками. Об этом я тоже поговорю во второй части.
  • Прямой подсчёт ссылок также означает, что значения можно расшаривать только между zval’ами. Например, строку невозможно совместно использовать в zval и ключе хэш-таблицы (без хранения этого ключа также в виде zval).

Zval’ы в PHP 7

In PHP 7 a zval can be reference counted or not. There is a flag in the zval structure which determined this.

There are some types which are never refcounted. These types are null, bool, int and double.

There are other types which are always refcounted. These are objects, resources and references.

And then there are types, which are sometimes refcounted. Those are strings and arrays.

For strings the not-refcounted variant is called an "interned string". If you're using an NTS (not thread-safe) PHP 7 build, which you typically are, all string literals in your code will be interned. These interned strings are deduplicated (i.e. there is only one interned string with a certain content) and are guaranteed to exist for the full duration of the request, so there is no need to use reference counting for them. If you use opcache, these strings will live in shared memory, in which case you can't use reference counting for them (as our refcounting mechanism is non-atomic). Interned strings have a dummy refcount of 1, which is what you're seeing here.

For arrays the not-refcounted variant is called an "immutable array". If you use opcache, then constant array literals in your code will be converted into immutable arrays. Once again, these live in shared memory and as such must not use refcounting. Immutable arrays have a dummy refcount of 2, as it allows us to optimize certain separation paths.

В седьмой версии языка мы получили новую реализацию zval. Одним из главных нововведений стало то, что zval больше не нужно отдельно размещать в куче. Также refcount теперь хранится не в самом zval, а в любом из комплексных значений, на которые он указывает — в строках, массивах или объектах. Это даёт следующие преимущества:

  • Простые значения не требуют размещения в куче и не используют подсчёт ссылок. А хранятся или в стеке, или в составе комплексных структур.
  • Больше нет никакого двойного подсчёта. В случае с объектами используется счётчик только внутри самого объекта.
  • Поскольку refcount теперь хранится в самом значении, то оно может быть использовано независимо от самого zval. Например, строка может использоваться и в zval, и быть ключом в хэш-таблице.
  • Теперь стало гораздо меньше указателей, которые нужно перебрать, чтобы получить значение.

Вот как выглядит структура нового zval:

    zend_value value;
    union {
        struct {
            ZEND_ENDIAN_LOHI_4(
                zend_uchar type,
                zend_uchar type_flags,
                zend_uchar const_flags,
                zend_uchar reserved)
        } v;
        uint32_t type_info;
    } u1;
    union {
        uint32_t var_flags;
        uint32_t next;                 // hash collision chain
        uint32_t cache_slot;           // literal cache slot
        uint32_t lineno;               // line number (for ast nodes)
        uint32_t num_args;             // arguments number for EX(This)
        uint32_t fe_pos;               // foreach position
        uint32_t fe_iter_idx;          // foreach iterator index
    } u2;
};`

Первый компонент остался практически таким же, это объединение value. Второй компонент — целочисленный, хранящий информацию о типе, который с помощью объединения разбит на отдельные байты (можно игнорировать макрос ZEND_ENDIAN_LOHI_4, он нужен лишь для обеспечения консистентной структуры между платформами с разными порядками следования байтов). Важными частями этой вложенной конструкции являются type и type_flags, о них я расскажу ниже.

Также здесь есть одна небольшая проблема. Value занимает 8 байт, и благодаря своей структуре добавление даже одного байта повлечёт за собой увеличение размера zval на 16 байт. Но ведь нам не нужно целых 8 байт для хранения типа. Поэтому в zval есть дополнительное объединение u2, которое по умолчанию не используется, но может применяться для хранения 4 байт данных. Разные компоненты объединения предназначены для разных видов использования этого дополнительного хранилища.

В PHP 7 объединение value несколько отличается от пятой версии:

typedef union _zend_value {
    zend_long         lval;
    double            dval;
    zend_refcounted  *counted;
    zend_string      *str;
    zend_array       *arr;
    zend_object      *obj;
    zend_resource    *res;
    zend_reference   *ref;
    zend_ast_ref     *ast;

    // Эти пока можно игнорировать, они специальные
    zval             *zv;
    void             *ptr;
    zend_class_entry *ce;
    zend_function    *func;
    struct {
        ZEND_ENDIAN_LOHI(
            uint32_t w1,
            uint32_t w2)
    } ww;
} zend_value;

Обратите внимание, что value теперь занимает 8 байт вместо 16. Оно хранит только целочисленные (lval) и числа с плавающей запятой (dval). Всё остальное — это указатель. Все типы указателей (за исключением специальных, отмеченных выше) используют подсчёт ссылок и содержат заголовок, определяемый zend_refcounted:

struct _zend_refcounted {
    uint32_t refcount;
    union {
        struct {
            ZEND_ENDIAN_LOHI_3(
                zend_uchar    type,
                zend_uchar    flags,
                uint16_t      gc_info)
        } v;
        uint32_t type_info;
    } u;
};

Я уже упоминал, что zval больше не нужно отдельно размещать в куче. Но их же нужно где-то хранить. Они всё ещё являются частью структур, размещаемых в кучах. Например, хэш-таблица будет содержать собственный zval вместо указателя на отдельный zval. Скомпилированная таблица переменных функции и таблица свойств объекта будут представлять собой zval-массивы. В качестве таких zval теперь обычно хранятся те, у которых косвенность на один уровень ниже. То есть zval’ом теперь называется то, что раньше было zval*.

Когда-то нужно было копировать zval* и инкрементить его refcount, чтобы использовать zval в новом месте. Теперь для этого достаточно скопировать содержимое zval (игнорируя u2) и, может быть, инкрементить refcount того значения, на которое он указывает, если значение использует подсчёт ссылок.

Откуда PHP знает, что используется подсчёт? Это нельзя определить по одному лишь типу, поскольку некоторые типы не используют refcount — например, строки и массивы. Для этого используется один бит компонента type_info.

Внутренние типы

Какие типы поддерживаются в PHP 7:

// обычные типы данных
#define IS_UNDEF                    0
#define IS_NULL                     1
#define IS_FALSE                    2
#define IS_TRUE                     3
#define IS_LONG                     4
#define IS_DOUBLE                   5
#define IS_STRING                   6
#define IS_ARRAY                    7
#define IS_OBJECT                   8
#define IS_RESOURCE                 9
#define IS_REFERENCE                10

// константные выражения
#define IS_CONSTANT                 11
#define IS_CONSTANT_AST             12

// внутренние типы
#define IS_INDIRECT                 15
#define IS_PTR                      17

В чём отличия от PHP 5:

  • Тип IS_UNDEF используется вместо указателя на zval NULL (не путайте с IS_NULL zval). Например, в приведённых выше примерах переменным назначается тип IS_UNDEF.
  • Тип IS_BOOL разделён на IS_FALSE и IS_TRUE. Поскольку такое булево значение теперь встроено в тип, это позволяет оптимизировать ряд проверок на основе типа. Данное изменение незаметно для пользователей, которые по-прежнему оперируют единственным «булевым» типом.
  • PHP-ссылки больше не используют в zval флаг is_ref. Вместо него введён новый тип IS_REFERENCE. Ниже я расскажу, как это работает.
  • IS_INDIRECT и IS_PTR являются специальными внутренними типами.

Тип IS_LONG вместо обычного long из языка С теперь использует значение zend_long. Причина в том, что в 64-битных Windows long имеет разрядность только 32 бита. Поэтому PHP 5 больше не использует в Windows в обязательном порядке 32-битные числа. А в PHP 7 вы можете использовать 64-битные значения, если система также 64-битная.

Массивы

На уровне PHP, массив — это упорядоченный список скрещенный с мэпом. Грубо говоря, PHP смешивает эти два понятия, в итоге получается, с одной стороны, очень гибкая структура данных, с другой стороны, далеко не самая оптимальная. На самом деле, для реализации массивов в PHP, используется вполне себе стандартная структура данных Hash Table. Hash Table хранит в себе указатель на самое первое и последнее значения (нужно для упорядочивания массивов), указатель на текущее значение (используется для итерации по массиву, это то, что возвращает current()), кол-во элементов, представленных в массиве, массив указателей на Bucket-ы (о них далее), и еще кое-что.

В Hash Table есть две главные сущности, первая — это собственно сам Hash Table, и вторая — это Bucket(ведол). В ведрах хранятся сами значения, то есть на каждое значение — свое ведро. Но помимо этого в ведре хранится оригинал ключа, указатели на следующее и предыдущее ведра (они нужны для упорядочивания массива, ведь в PHP ключи могут идти в любом порядке, в каком вы захотите), и, опять же, еще кое-что.

Таким образом, когда вы добавляете новый элемент в массив, если такого ключа там еще нет, то под него создается новое ведро и добавляется в Hash Table. Как было сказано выше, у HT есть некий массив указателей на ведра, при этом ведра доступны в этом массиве по некоему индексу, а этот индекс можно вычислить зная ключ ведра.

struct _zend_array {
    zend_refcounted_h gc;
    union {
        struct {
            ZEND_ENDIAN_LOHI_4(
                zend_uchar    flags,
                zend_uchar    nApplyCount,
                zend_uchar    nIteratorsCount,
                zend_uchar    reserve)
        } v;
        uint32_t flags;           /* доступно 32 флага */
    } u;
    uint32_t     nTableMask;       /* маска — nTableSize */
    Bucket      *arData;           /* полезное хранилище данных */
    uint32_t     nNumUsed;         /* следующая доступная ячейка в arData */
    uint32_t     nNumOfElements;   /* общее количество занятых элементов в arData */
    uint32_t     nTableSize;       /* размер таблицы, всегда равен двойке в степени */
    uint32_t     nInternalPointer; /* используется для итерации */
    zend_long    nNextFreeElement; /* следующий доступный целочисленный ключ */
    dtor_func_t  pDestructor;      /* деструктор данных */
};

Самое интересное поле данных — arData, это своеобразный указатель на область памяти цепочки Bucket. Сама Bucket представляет собой одну ячейку в массиве:

typedef struct _Bucket {
    zval              val; /* значение */
    zend_ulong        h;   /* хэш (или числовой индекс)   */
    zend_string      *key; /* строковый ключ или NULL для числовых значений */
} Bucket;

Как вы могли заметить, в структуре Bucket будет храниться zval. Обратите внимание, что здесь используется не указатель на zval, а именно сама структура. Так сделано потому, что в РНР 7 zval’ы больше не размещаются в куче (в отличие от PHP 5), но при этом в РНР 7 может размещаться целевое значение, хранящееся в zval в виде указателя (например, строка РНР).

Стоит отметить, что в PHP почти все посторено на одной этой структуре HashTable: все переменные, лежащие в каком-либо scope-е, на самом деле лежат в HT, все методы классов, все поля классов, даже сами дефинишины классов лежат в HT, это на самом деле очень гибкая структура. Помимо прочего, HT обеспечивает практически одинаковую скорость выборки/вставки/удаления и сложность всех троих является O(1), но с оговоркой на небольшой оверхед при коллизиях.

Свойства хэш таблицы:

  • Ключ может быть строкой или целочисленным. В первом случае используется структура zend_string, во втором — zend_ulong.
  • Хэш-таблица всегда должна помнить порядок добавления её элементов.
  • Размер хэш-таблицы меняется автоматически. В зависимости от обстоятельств она самостоятельно уменьшается или увеличивается.
  • С точки зрения внутренней реализации размер таблицы всегда равен двойке в степени. Это делается для улучшения производительности и выравнивания размещения данных в памяти.
  • Все значения в хэш-таблице хранятся в структуре zval, больше нигде. Zval’ы могут содержать данные любых типов.

Разрешение коллизий

Теперь разберёмся, как разрешаются коллизии. Как вы помните, в хэш-таблице несколько ключей при хэшировании и сжатии могут соответствовать одному и тому же индексу преобразования. Так что, получив индекс преобразования, мы с его помощью извлекаем данные обратно из arData и сравниваем хэши и ключи, проверяя, то ли это, что нужно. Если данные неправильные, мы проходим по связному списку с помощью поля zval.u2.next, в котором отражается следующая ячейка для внесения данных.

Обратите внимание, что связный список не рассеян по памяти, как традиционные связные списки. Вместо того чтобы ходить по нескольким размещённым в памяти указателям, полученным от кучи — и наверняка разбросанным по адресному пространству, — мы считываем из памяти полный вектор arData И это одна из главных причин увеличения производительности хэш-таблиц в РНР 7, а также всего языка.

В РНР 7 у хэш-таблиц очень высокая локальность данных. В большинстве случаев доступ происходит за 1 наносекунду, поскольку данные обычно находятся в процессорном кэше первого уровня.

Строки

В PHP 7 строки представляются с помощью типа zend_string:

struct _zend_string {
    zend_refcounted   gc;
    zend_ulong        h;        /* hash value */
    size_t            len;
    char              val[1];
};

Помимо содержащегося в заголовке refcounted, здесь также используется кэш хэша h, длина len и значение val. Кэш хэша используется для того, чтобы не пересчитывать хэш строки при каждом обращении к HashTable. При первом использовании он инициализируется как ненулевой хэш. Если вы не слишком хорошо знакомы с разнообразными хаками в языке С, то определение val может показаться странным: он объявляется как массив символов с одним-единственным элементом. Но мы же наверняка захотим хранить строки длиной больше, чем один символ. Здесь используется метод под названием «структурный хак» (struct hack): массив хоть и объявляется с одним элементом, но при создании zend_string мы определим возможность хранения более длинной строки. Кроме того, можно будет получить доступ к более длинным строкам с помощью val.

Новая реализация строковой переменной имеет ряд преимуществ перед обычными строками в языке С. Во-первых, в неё теперь интегрирована длина, которая больше не «болтается» где-то поблизости. Во-вторых, в заголовке используется подсчёт ссылок, поэтому стало возможным использовать строки в разных местах без применения zval. Это особенно важно для расшаривания ключей хэш-таблицы.

Но есть и большая ложка дёгтя. Получить из zend_string строку языка С легко (с помощью str->val), а вот из С-строки напрямую получить zend_string нельзя. Для этого придётся скопировать значение строки в заново созданный zend_string. Особенно досадно, когда дело доходит до работы с текстовыми строками (literal string), то есть постоянными строками (constant string), встречающимися в исходном С-коде.

Изолированные (interned) строки — это такие строки, которые не уничтожаются до завершения запроса и потому не нуждаются в использовании счётчика ссылок. Они дедуплицированы, поэтому при создании новой изолированной строки движок сначала проверяет, нет ли другой с таким же значением. Вообще все строки, имеющиеся в PHP-коде (включая переменные, названия функций и т.д.), обычно являются изолированными. Неизменяемые строки — это изолированные строки, созданные до начала запроса. Они не уничтожаются по окончании запроса, в отличие от изолированных.

Если используется OPCache, то изолированные строки будут храниться в общей памяти (SHM) и использоваться всеми PHP-процессами. В этом случае неизменяемые строки становятся бесполезными, поскольку изолированные и так не будут уничтожены.

Классы и объекты

Классы:

  • Классы занимают много памяти.
  • Внутренние классы гораздо лучше оптимизированы по сравнению с пользовательскими, потому что последние должны быть созданы и уничтожены при каждом запросе. Внутренние классы существуют постоянно.
  • Классы, интерфейсы и трейты используют одни и те же структуру и процедуры, различия очень малы.
  • Во время наследования или объявления процесс привязки сильно и долго нагружает процессор, но памяти задействуется немного, поскольку многие вещи не дуплицируются, а используются совместно. Кроме того, лучше запускать привязку классов во время компиляции.

Объекты

PHP7 отказались от двойного подсчёта ссылок, уменьшили потребление памяти и количество косвенной адресации. Так выглядит новая структура zend_object:

struct _zend_object {
    zend_refcounted   gc;
    uint32_t          handle;
    zend_class_entry *ce;
    const zend_object_handlers *handlers;
    HashTable        *properties;
    zval              properties_table[1];
};

Эта структура — почти всё, что осталось от объекта. zend_object_value, заменённый прямым указателем на объект и хранилище объектов, хоть и не исключён совсем, но на глаза попадается куда реже.

Улучшение производительности PHP7

  • Упакованные массивы — Они потребляют меньше памяти и во многих случаях работают гораздо быстрее традиционных массивов. Упакованные массивы должны удовлетворять критериям:

    • Ключи — только целочисленные значения;
    • Ключи вставляются в массив только по возрастанию.

    Не забывайте: Если вам нужен список, то не используйте строки в ключах (это не даст применять оптимизацию упакованных массивов); Если ключи в списке только целочисленные, постарайтесь распределить их по возрастанию (в противном случае оптимизация тоже не сработает).

  • Неизменяемые массивы - они являются частью расширения OPCache. Неизменяемые массивы — это массивы, заполненные неизменяемыми типами (нет переменных, нет вызовов функций, всё решается во время компиляции), которые не дуплицируются в памяти при переносе из одного места в другое в ходе runtime’а PHP. Также эти типы никогда не уничтожаются в памяти (от одного запроса к другому).

  • Оптимизация encapsed-строк

  • Целочисленные и значения с плавающей запятой в PHP 7 бесплатны

    В PHP 7 совершенно иной способ размещения переменных в памяти. Вместо кучи они теперь хранятся в пулах стековой памяти. У этого есть побочный эффект: вы можете бесплатно повторно использовать контейнеры переменных (variable containers), память не выделяется. В PHP 5 такое невозможно, там для каждого создания/присвоения переменной нужно выделить немного памяти (что ухудшает производительность). В седьмой версии использование целочисленных и значений с плавающей запятой совершенно бесплатно: память нужного размера уже выделена для самих контейнеров переменных.

  • Reference mismatch

Дополнительно: