Управление памятью

Чтобы правильно и эффективно работать с zval’ами, важно понимать, как устроено управление их памятью. В общих чертах значения можно разделить на две категории: простые значения, такие как целые числа, которые хранятся непосредственно внутри zval’а, и сложные значения, такие как строки, для которых zval хранит только указатель на отдельную структуру.

Значения с подсчётом ссылок

Все сложные значения имеют общий заголовок следующей структуры:

typedef struct _zend_refcounted_h {
    uint32_t refcount;
    union {
        uint32_t type_info;
    } u;
} zend_refcounted_h;

Этот заголовок хранит счётчик ссылок, который отслеживает, в скольких местах используется эта структура. Если структура используется в новом zval’е, счётчик ссылок увеличивается. Если она больше не используется, счётчик уменьшается. Если счётчик ссылок достигает нуля, мы знаем, что структура больше не используется, и её можно освободить. Это основной механизм управления памятью в PHP.

Поле type_info кодирует дополнительную информацию, такую как тип структуры, ряд специфичных для типа флагов, а также корень сборки мусора. Назначение этой информации мы обсудим позже.

Существуют функции для создания различных видов структур с подсчётом ссылок, которые создают их с начальным счётчиком ссылок равным единице:

zend_string *str = zend_string_init("test", sizeof("test")-1, /* persistent */ 0); // refcount=1
zend_array *arr = zend_new_array(/* size hint */ 0); // refcount=1

// Do something with str and arr.

zend_string_release(str); // refcount=0 => destroy!
zend_array_release(arr); // refcount=0 => destroy!

Функции zend_string_release() и zend_array_release() уменьшают счётчик ссылок строки или массива, и если он достигает нуля, уничтожают структуру. Например, следующий код совершенно корректен:

zend_string *str = zend_string_init("test", sizeof("test")-1, /* persistent */ 0); // refcount=1
zend_hash_add_empty_element(arr, str); // refcount=2
zend_string_release(str); // refcount=1

Здесь в массив добавляется элемент с ключом str, а затем строка освобождается. Однако функция zend_hash_add_empty_element() увеличила счётчик ссылок строки, поэтому вызов zend_string_release() не уничтожит её. Она будет уничтожена только тогда, когда будет уничтожен сам массив, и на строку не останется других ссылок.

Неизменяемые значения

Хотя все сложные структуры имеют общий заголовок zend_refcounted_h, счётчик ссылок не всегда используется по-настоящему. Строки и массивы могут быть неизменяемыми (immutable), что означает, что всю структуру, включая счётчик ссылок, никогда нельзя изменять. Такие структуры можно переиспользовать без увеличения счётчика ссылок, и гарантируется, что они не будут уничтожены (как минимум) до конца запроса.

Есть несколько причин, по которым существуют неизменяемые строки и массивы:

  • Любые структуры, хранящиеся в общей памяти opcache, неизменяемы, поскольку являются общими для нескольких процессов. Вы можете установить ini-настройку opcache.protect_memory=1, чтобы принудительно обеспечить это через mprotect(). Это приведёт к тому, что большинство нарушений неизменяемости вызовут крэш, а не неправильное поведение.

  • Пустой массив объявлен const и поэтому, как правило, выделяется в сегменте, доступном только для чтения. Попытка изменить его приведёт к крэшу.

  • Постоянные строки, создаваемые вне запроса, но потенциально используемые внутри него (например, значения ini), должны быть неизменяемыми, поскольку их могут параллельно использовать несколько потоков. Поскольку подсчёт ссылок в PHP неатомарен, обычный подсчёт ссылок был бы небезопасен.

  • Наконец, хотя приведённые выше причины делают неизменяемые структуры технически необходимыми, их наличие также служит оптимизацией производительности, так как операции подсчёта ссылок можно избежать во многих распространённых случаях.

При работе с высокоуровневыми API, такими как zend_string_copy() или ZVAL_COPY(), неизменяемые структуры обрабатываются автоматически и корректно. Однако если вы используете низкоуровневые API, вам нужно учитывать это явно.

Низкоуровневый интерфейс предоставляется в основном через следующие макросы:

Макрос

Описание

GC_TYPE

Получить тип структуры (константа IS_*).

GC_FLAGS

Получить флаги.

GC_REFCOUNT

Получить счётчик ссылок.

GC_ADDREF

Увеличить счётчик ссылок. Структура должна быть изменяемой.

GC_DELREF

Уменьшить счётчик ссылок. Структура должна быть изменяемой. Не освобождает структуру при достижении счётчиком нуля.

GC_TRY_ADDREF

Увеличить счётчик ссылок, если структура изменяемая, иначе ничего не делать.

Неизменяемые структуры устанавливают флаг GC_IMMUTABLE (у которого есть ряд псевдонимов, таких как IS_STR_INTERNED и IS_ARRAY_IMMUTABLE), который можно использовать, чтобы определить, безопасно ли увеличивать счётчик ссылок:

zend_string *str = /* ... */;

if (!(GC_FLAGS(str) & GC_IMMUTABLE)) {
    GC_ADDREF(str);
}

// Same as:
GC_TRY_ADDREF(str);

// Same as (high-level API):
zend_string_addref(str);

Макросы, в названии которых есть TRY, обычно означают, что операция должна выполняться только для изменяемых структур. Вы встретите больше подобных примеров, таких как Z_TRY_ADDREF и GC_TRY_PROTECT_RECURSION, где смысл такой же.

Постоянные структуры

PHP использует два аллокатора: аллокатор, привязанный к запросу, который освобождает всю память в конце запроса, и постоянный аллокатор, который сохраняет выделения между несколькими запросами. Постоянный аллокатор фактически совпадает с обычным системным аллокатором. См. главы Жизненный цикл PHP и Менеджер памяти Zend для получения дополнительной информации об управлении выделением памяти в PHP.

Многие функции, создающие структуры с подсчётом ссылок, принимают флаг persistent, определяющий, какой аллокатор использовать. Пример этого — последний аргумент zend_string_init(). Если функция не предоставляет флаг persistent, хорошим предположением по умолчанию будет то, что используется аллокатор, привязанный к запросу (непостоянный). Например, функция zend_array_new() всегда создаёт массив, привязанный к запросу, в то время как для создания постоянного массива нужно использовать низкоуровневые API.

Постоянные структуры устанавливают флаг GC_PERSISTENT, и их деструкторы автоматически заботятся об использовании правильного аллокатора для освобождения памяти. Поэтому вам, как правило, не нужно беспокоиться об этом флаге, если изначально был использован правильный аллокатор (обычно привязанный к запросу).

Однако важно понимать, как постоянные структуры взаимодействуют с кодом, выполняемым во время запроса: постоянные структуры потенциально могут использоваться несколькими потоками. Поскольку подсчёт ссылок в PHP неатомарен, выполнение подсчёта ссылок из нескольких потоков приводит к состоянию гонки (data race), что приводит к крэшам.

Поэтому любая постоянная структура, которая также используется во время запроса, должна быть либо неизменяемой, либо локальной для потока (thread-local). PHP можно собрать с CFLAGS="-DZEND_RC_DEBUG=1", чтобы автоматически диагностировать такие проблемы. Эта проблема чаще всего затрагивает строки, и в этом случае их можно сделать неизменяемыми с помощью интернирования. Макрос GC_MAKE_PERSISTENT_LOCAL() используется, чтобы пометить постоянную структуру как локальную для потока. Этот макрос не делает ничего, кроме отключения проверки ZEND_RC_DEBUG.

Управление памятью zval’ов

Разобравшись с предварительными сведениями, мы можем обсудить, как управление памятью взаимодействует с zval’ами. Структуры с подсчётом ссылок можно использовать независимо, но хранение их внутри zval’ов — безусловно, один из самых распространённых случаев использования.

Сами zval’ы никогда не выделяются в куче индивидуально. Они либо временно выделяются на стеке, либо встраиваются как часть более крупной структуры, выделенной в куче.

Этот простой пример показывает инициализацию zval’а, выделенного на стеке, и его последующее уничтожение:

zval str_val;
ZVAL_STRING(&str_val, "foo"); // Creates zend_string (refcount=1).
// ... Do something with str_val.
zval_ptr_dtor(&str_val); // Decrements to refcount=0, and destroys the string.

ZVAL_STRING() создаёт строковый zval, а zval_ptr_dtor() освобождает его. Различные макросы инициализации и деструкторы мы обсудим чуть позже.

Zval, выделенный на стеке, можно использовать только в той области видимости, в которой он был объявлен. Хотя технически возможно вернуть zval, вы обнаружите, что PHP никогда не передаёт и не возвращает zval’ы по значению. Вместо этого zval’ы всегда передаются по указателю. Чтобы вернуть zval, в функцию нужно передать выходной параметр:

// retval is an output parameter.
void init_zval(zval *retval) {
    ZVAL_STRING(retval, "foo");
}

void some_other_function() {
    zval val;
    init_zval(&val);
    // ... Do something with val.
    zval_ptr_dtor(&val);
}

Хотя сами zval’ы, как правило, не являются общими, можно сделать общими структуры, на которые они указывают, с помощью механизма подсчёта ссылок. Макросы Z_REFCOUNT, Z_ADDREF и Z_DELREF работают так же, как соответствующие макросы GC_*, но оперируют zval’ами. Важно, что эти макросы можно использовать только если zval действительно указывает на структуру с подсчётом ссылок, и эта структура не является неизменяемой. Флаг типа IS_TYPE_REFCOUNTED определяет, так ли это, и доступ к нему можно получить через Z_REFCOUNTED:

void fill_array(zval *array) {
    zval val;
    init_zval(&val);

    // Manually check REFCOUNTED:
    if (Z_REFCOUNTED(val)) {
        Z_ADDREF(val);
    }
    add_index_zval(array, 0, &val);

    // Or use the TRY macro:
    Z_TRY_ADDREF(val);
    add_index_zval(array, 1, &val);

    zval_ptr_dtor(&val);
}

В этом примере одно и то же значение добавляется в массив дважды, то есть счётчик ссылок нужно увеличить два раза. Хотя можно вручную проверять, является ли zval Z_REFCOUNTED, предпочтительнее использовать Z_TRY_ADDREF, который увеличивает счётчик ссылок только для структур с подсчётом ссылок.

Здесь стоит задуматься о том, кто отвечает за увеличение счётчика ссылок. В этом примере за увеличение отвечает вызывающий код add_index_zval(). К сожалению, API PHP не очень последовательны в этом отношении. Как очень грубое правило: значения массивов ожидают, что счётчик ссылок будет увеличен вызывающей стороной, в то время как большинство других API сами заботятся об этом.

Копирование zval’ов

Zval’ы часто нужно копировать из одного места в другое. Для этого предусмотрен ряд макросов копирования. Первый — ZVAL_COPY_VALUE():

void init_zval_indirect(zval *retval) {
    zval val;
    init_zval(&val);
    ZVAL_COPY_VALUE(retval, &val);
}

Этот (довольно бессмысленный) пример инициализирует zval на стеке, а затем переносит значение в выходной параметр retval. Макрос ZVAL_COPY_VALUE выполняет простое копирование zval’а без увеличения счётчика ссылок. Поэтому его основное назначение — перемещение zval’а, что означает, что исходный zval больше не будет использоваться (в том числе его не следует уничтожать). Иногда этот макрос также используется как оптимизация для копирования zval’а, про который мы знаем, что он не относится к структурам с подсчётом ссылок.

Макрос ZVAL_COPY_VALUE отличается от простого присваивания (*retval = val) тем, что копирует только значение и тип zval’а, но не его поле u2. Поэтому безопасно применять ZVAL_COPY_VALUE к zval’у, чьё поле u2 используется — оно не будет перезаписано.

Второй макрос — ZVAL_COPY, представляющий собой оптимизированную комбинацию ZVAL_COPY_VALUE и Z_TRY_ADDREF:

void init_pair(zval *retval1, zval *retval2) {
    zval val;
    init_zval(&val); // refcount=1

    ZVAL_COPY(retval1, &val); // refcount=2
    ZVAL_COPY(retval2, &val); // refcount=3

    zval_ptr_dtor(&val); // refcount=2
}

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

void init_pair(zval *retval1, zval *retval2) {
    zval val;
    init_zval(&val); // refcount=1
    ZVAL_COPY(retval1, &val); // refcount=2
    ZVAL_COPY_VALUE(retval2, &val); // refcount=2
}

Здесь значение копируется один раз в retval1, а затем перемещается в retval2, что экономит лишнее увеличение и уменьшение счётчика ссылок. Наконец, вот как мы, вероятно, написали бы этот код на практике:

void init_pair(zval *retval1, zval *retval2) {
    init_zval(retval1); // refcount=1
    ZVAL_COPY(retval2, retval1); // refcount=2
}

Здесь значение инициализируется непосредственно в retval1, а затем копируется в retval2. Этот вариант и самый простой, и самый эффективный.

Макрос ZVAL_DUP похож на ZVAL_COPY, но дублирует массивы, а не просто увеличивает их счётчик ссылок. Если вы используете этот макрос, вы почти наверняка делаете что-то совершенно неправильное.

Наконец, ZVAL_COPY_OR_DUP — это очень специализированный макрос копирования, который можно использовать при копировании из потенциально постоянного zval’а во время запроса. Как упоминалось ранее, увеличивать счётчик ссылок в этом случае недопустимо, поскольку это было бы небезопасно в многопоточной среде. Этот макрос увеличит счётчик ссылок для непостоянных значений, но выполнит полное дублирование строки/массива для постоянных значений.

Уничтожение zval’ов

В приведённых выше примерах уже использовалась функция zval_ptr_dtor() для уничтожения zval’ов. Если значение является структурой с подсчётом ссылок, эта функция уменьшает счётчик ссылок и уничтожает значение, когда он достигает нуля.

Однако здесь есть один тонкий момент: подсчёта ссылок недостаточно для обнаружения неиспользуемых значений, которые являются частью циклов. По этой причине PHP использует дополнительный циклический сборщик мусора (garbage collector, GC) в стиле mark-and-sweep. Когда счётчик ссылок уменьшается, но не достигает нуля, и структура помечена как потенциально циклическая (флаг GC_NOT_COLLECTABLE не установлен), PHP добавляет структуру в буфер корней сборщика мусора.

Функция zval_ptr_dtor_nogc() — это вариант, который не выполняет проверки буфера корней сборщика мусора, и его безопасно использовать только если вы знаете, что уничтожаемые данные нециклические. zval_dtor() — это устаревший псевдоним той же функции.

Другой вариант, который можно встретить во внутреннем коде, — i_zval_ptr_dtor(), который делает то же самое, что и zval_ptr_dtor(), но использует встраиваемую (inline) реализацию. Префикс i_ — это общее соглашение для функций, у которых есть как встраиваемые, так и невстраиваемые варианты.

Инициализация zval’ов

До сих пор мы использовали абстрактную функцию init_zval(), которая каким-то образом инициализирует zval. Не удивительно, что PHP обрабатывает инициализацию zval’ов с помощью множества макросов. Инициализация простых типов особенно проста:

zval val;
ZVAL_UNDEF(&val);

zval val;
ZVAL_NULL(&val);

zval val;
ZVAL_FALSE(&val);

zval val;
ZVAL_TRUE(&val);

zval val;
ZVAL_BOOL(&val, zero_or_one);

zval val;
ZVAL_LONG(&val, 42);

zval val;
ZVAL_DOUBLE(&val, 3.141);

Для строк существует довольно много вариантов инициализации. Самый фундаментальный — макрос ZVAL_STR(), который принимает уже созданный zend_string*:

zval val;
ZVAL_STR(&val, zend_string_init("test", sizeof("test")-1, 0));

Поскольку создание zend_string из строкового литерала или существующей строки — очень распространённая операция, существуют два вспомогательных макроса-обёртки:

zval val;
ZVAL_STRINGL(&val, "test", sizeof("test")-1);

zval val;
ZVAL_STRING(&val, "test"); // Uses strlen() for length.

Макрос ZVAL_STR установит флаг IS_TYPE_REFCOUNTED в зависимости от того, является ли строка неизменяемой или нет. Существуют два оптимизированных варианта, которые можно использовать, если заранее известно, что строка интернирована:

// This string is definitely not interned/immutable.
zval val;
ZVAL_NEW_STR(&val, zend_string_init("test", sizeof("test")-1, 0));

// This string is definitely interned.
zval val;
ZVAL_INTERNED_STR(&val, ZSTR_CHAR('a'));

Для пустых строк существует отдельный помощник:

zval val;
ZVAL_EMPTY_STRING(&val);

Макрос ZVAL_STRINGL_FAST можно использовать, чтобы избежать выделения zend_string, если строка пуста или состоит из одного символа, поскольку для таких строк всегда существуют интернированные варианты, которые можно быстро получить:

zval val;
ZVAL_STRINGL_FAST(&val, str, len);

Наконец, макрос ZVAL_STR_COPY представляет собой комбинацию ZVAL_STR и zend_string_copy, где последняя увеличивает счётчик ссылок строки:

zval val;
ZVAL_STR_COPY(&val, zstr); // Refcount will be incremented.
// More efficient/compact version of:
ZVAL_STR(&val, zend_string_copy(zstr));

Для массивов, к счастью, нужно рассмотреть только два макроса инициализации:

zval val;
ZVAL_ARR(&val, zend_new_array(/* size_hint */ 0));

zval val;
ZVAL_EMPTY_ARRAY(&val);

Первый инициализирует массивный zval существующей структурой zend_array*, а второй инициализирует именно пустой массив. Обратите внимание, что хотя оба приведённых выше примера инициализируют пустой массив, это не одно и то же. ZVAL_EMPTY_ARRAY() использует неизменяемый общий пустой массив, в то время как zend_new_array() создаёт новый. Если вы планируете впоследствии напрямую изменять массив, следует использовать вариант с zend_new_array().

Объектные zval’ы инициализируются с помощью ZVAL_OBJ:

zval val;
ZVAL_OBJ(&val, obj_ptr);

zval val;
ZVAL_OBJ_COPY(&val, obj_ptr); // Increments refcount

Хотя эти макросы довольно распространены при работе с уже существующими объектами, типичным способом создания объекта с нуля является object_init_ex(). Это будет рассмотрено в одной из следующих глав об объектах.

Наконец, ресурсы инициализируются с помощью ZVAL_RES:

zval val;
ZVAL_RES(&val, zend_register_resource(ptr, le_resource_type));

Разделение zval’ов

В PHP все значения по умолчанию следуют семантике передачи по значению. Это означает, что если вы напишете $a = $b, то изменение $a не повлияет на $b, и наоборот. При этом $a = $b реализуется, по сути, как:

zval_ptr_dtor(a);
ZVAL_COPY(a, b);

То есть $a и $b оба будут указывать на одну и ту же структуру с увеличенным счётчиком ссылок. Это означает, что наивное изменение $a также изменило бы $b.

Здесь вступает в игру концепция copy-on-write: вам разрешено изменять только те структуры, которыми вы владеете исключительно, то есть у которых счётчик ссылок равен единице. Если у структуры счётчик ссылок больше единицы, её сначала нужно разделить (separate). Разделение — это просто красивое слово для дублирования структуры.

На практике “структуру” можно заменить на “массив”. Хотя теоретически эта концепция применима и к строкам, строки в PHP почти никогда не изменяются после создания. Поэтому SEPARATE_ARRAY() является основным макросом разделения, который можно применять только к zval’ам типа IS_ARRAY:

zval a, b;
ZVAL_ARR(&b, zend_new_array(0));
ZVAL_COPY(&a, &b);

SEPARATE_ARRAY(&b); // b now holds a separate copy of the array.
// Modification of b will no longer affect a.

Макрос SEPARATE_ARRAY() заботится не только об общих массивах, но и о неизменяемых:

zval val;
ZVAL_EMPTY_ARRAY(&val); // Immutable empty array.
SEPARATE_ARRAY(&val); // Mutable copy of empty array.

Макрос SEPARATE_ZVAL_NOREF() разделяет обычный zval, но редко бывает полезен, поскольку разделение обычно непосредственно предшествует модификации, а для выполнения любой значимой модификации вам в любом случае нужно знать тип zval’а.

Объекты и ресурсы не требуют разделения, так как у них ссылочная (reference-like) семантика.