PHP Internals Book [русский перевод]

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

Структура данных zval играет 2 роли. Во-первых, как было описано в предыдущем разделе, она хранит данные и их тип. Во-вторых (это будет рассмотрено в текущем разделе) используется для эффективного управления значениями в памяти.

В этом разделе мы рассмотрим концепции подсчета ссылок и копирования-при-записи (copy-on-write), чтобы понять как их использовать в коде расширений.

Семантика знечений и ссылок

В PHP все значения всегда имеют семантику значений (value-semantics), только если вы явно не запросили использование ссылок. Это значит, что и при передаче значения в функцию, и при выполнении операции присваивания вы будете работать с 2 разными копиями значения. Пара примеров поможет убедиться в этом:

<?php

$a = 1;
$b = $a;
$a++;

// Только $a будет увеличена на 1, $b сохранит исходное значение:
var_dump($a, $b); // int(2), int(1)

function inc($n) {
    $n++;
}

$c = 1;
inc($c);

// Значение переменной $c снаружи функции и значение $n  внутри функции — это разные значения
var_dump($c); // int(1)

Пример выше очень прост и очевиден, но важно понимать, что это основное правило, применяемое везде. Оно также применимо и к объектам:

<?php

$obj = (object) ['value' => 1];

function fnByVal($val) {
    // Меняется не только значение переменной, но и тип с object на integer
    $val = 100;
}

function fnByRef(&$ref) {
    $ref = 100;
}

// Функция, в которую передано значение, не изменила $obj, а функция в которую передана ссылка — изменила:

fnByVal($obj);
var_dump($obj); // stdClass(value => 1), функция fnByVal не изменила переданный оъект
fnByRef($obj);
var_dump($obj); // int(100)

Часто можно услышать, что в PHP 5 объекты автоматически передаются по ссылке, но пример выше показывает, что это не так. Функция, в которую передается значение не может изменить значение переданной переменной, только функция, в которую передается ссылка, может сделать это.

Это так и есть, хотя объекты действительно ведут себя так, будто они переданы по ссылке. Вы не можете присвоить переменной другое значение, но вы можете менять свойства объекта. Это возможно потому, что значением объекта является ID, который используется для поиска "реальных данных" объекта. Семантика передачи по значению не даст вам изменить этот ID на другой или поменять тип переменной, но она не помешает вам изменить "реальные данные" объекта.

Немного изменим пример выше:

<?php

$obj = (object) ['value' => 1];

function fnByVal($val) {
    // Теперь мы не меняем тип переменной, а только одно из свойств объекта
    $val->value = 100;
}

var_dump($obj); // stdClass(value => 1)
fnByVal($obj);
var_dump($obj); // stdClass(value => 100), функция fnByVal изменила свойство в переданном объекте

То же самое можно сказать и про ресурсы, так как они тоже хранят только ID, который может быть использован для поиска данных. Итак, еще раз, семантика передачи по значению не дает вам изменить ID или тип zval-а, но не мешает вам сменить данные ресурса (например сдвинуть позицию в файле).

Подсчет ссылок и копирование-при-записи

Если вы немного поразмышляете о написанном выше, то вы придете к заключению, что PHP должен выполнять огромное число операций копирования. Каждый раз передавая переменную в функцию её значение должно быть скопировано. Это может не быть проблемой для данных типа integer или double, но представьте, что вы передаете в функцию массив, содержащий десять миллионов значений. Копирование миллионов значений при каждом вызове функции — это недопустимо медленно.

Для того чтобы избежать этого в PHP используется парадигма копирования-при-записи (copy-on-write). Zval может совместно использоваться множеством переменных/функций/и т.д., но только до тех пор пока данные zval-а используются для чтения. Как только кто-то захочет изменить данные zval-а, он будет скоприрован прежде чем изменения будут применены.

Так как один zval может быть использован в нескольких местах, PHP должен иметь возможность определить момент, кода zval больше никем не используется и удалить его (освободить занимаемую им память). PHP делает это простым подсчетом ссылок. Учтите, что "ссылка" здесь это не ссылка в терминах PHP (та, что задается при помощи &), а просто показатель, говорящий что кто-то (переменная, функция, и т.д.) использует этот zval. Число таких ссылок называется refcount и оно хранится в члене-данных refcount__gc zval-а.

Чтобы понять как это работает давайте рассмотрим пример:

<?php

$a = 1;    // $a =           zval_1(value=1, refcount=1)
$b = $a;   // $a = $b =      zval_1(value=1, refcount=2)
$c = $b;   // $a = $b = $c = zval_1(value=1, refcount=3)

$a++;      // $b = $c = zval_1(value=1, refcount=2)
           // $a =      zval_2(value=2, refcount=1)

unset($b); // $c = zval_1(value=1, refcount=1)
           // $a = zval_2(value=2, refcount=1)

unset($c); // zval_1 is destroyed, because refcount=0
           // $a = zval_2(value=2, refcount=1)

Логика здесь простая: когда ссылка добавляется, значение refcount увеличивается на единицу, когда ссылка удаляется — refcount уменьшается. Когда значение refcount достигает 0 — zval удаляется.

Правда, этот метод не будет работать в случае циклических ссылок:

<?php

$a = []; // $a = zval_1(value=[], refcount=1)
$b = []; // $b = zval_2(value=[], refcount=1)

$a[0] = $b; // $a = zval_1(value=[0 => zval_2], refcount=1)
            // $b = zval_2(value=[], refcount=2)
            // refcount zval_2 увеличен так как
            // он использован в массиве zval_1

$b[0] = $a; // $a = zval_1(value=[0 => zval_2], refcount=2)
            // $b = zval_2(value=[0 => zval_1], refcount=2)
            // refcount zval_1 увеличен так как
            // он использован в массиве zval_2

unset($a);  //      zval_1(value=[0 => zval_2], refcount=1)
            // $b = zval_2(value=[0 => zval_1], refcount=2)
            // refcount  zval_1 уменьшен, но zval
            // продолжит существовать так как на него все еще ссылается zval_2

unset($b);  //      zval_1(value=[0 => zval_2], refcount=1)
            //      zval_2(value=[0 => zval_1], refcount=1)
            // refcount zval_2 уменьшен, но
            // продолжит существовать так как на него все еще ссылается zval_1

После того как код приведенный выше будет запущен мы получим ситуацию, в которой у нас будет два zval-а недоступных ни через одну переменную, но все еще существующих в памяти, так как они ссылаются друг на друга. Это классический пример проблемы с подсчетом ссылок.

Для решения этой проблемы в PHP реализован еще один механизм сборки мусора — циклический сборщик. Мы можем его сейчас проигнорировать так как циклический сборщик (в отличии от механизма подсчета ссылок) прозрачен для разработчиков расширений PHP. Если вам интересна эта тема, то обратитесь к документации PHP, в которой описан этот алгоритм.

Есть еще одна особенность PHP-ссылок (тех, что определяются как &$var, а не тех, что были рассмотрены выше), которая должна быть рассмотрена. Для того чтобы обозначить, что zval используется как PHP-ссылка используется флаг is_ref__gc в структуре zval.

Если is_ref=1 это является сигналом к тому, что zval не должен быть скопирован перед модификацией, вместо этого должно быть изменено значение zval-а:

<?php

$a = 1;   // $a =      zval_1(value=1, refcount=1, is_ref=0)
$b =& $a; // $a = $b = zval_1(value=1, refcount=2, is_ref=1)

$b++;     // $a = $b = zval_1(value=2, refcount=2, is_ref=1)
          // Так как is_ref=1 PHP напрямую изменяет zval
          // вместо того чтобы делать его копию

В примере выше zval переменной $a перед созданием ссылки имеет refcount=1. Теперь рассмотрим похожий пример с числом ссылок большим чем 1:

<?php

$a = 1;   // $a =           zval_1(value=1, refcount=1, is_ref=0)
$b = $a;  // $a = $b =      zval_1(value=1, refcount=2, is_ref=0)
$c = $b   // $a = $b = $c = zval_1(value=1, refcount=3, is_ref=0)

$d =& $c; // $a = $b = zval_1(value=1, refcount=2, is_ref=0)
          // $c = $d = zval_2(value=1, refcount=2, is_ref=1)
          // $d это ссылка $c, но *не* на $a and $b, поэтому
          // zval здесь должен быть скопирован. Теперь у нас есть
          // один zval с is_ref=0 и один is_ref=1.

$d++;     // $a = $b = zval_1(value=1, refcount=2, is_ref=0)
          // $c = $d = zval_2(value=2, refcount=2, is_ref=1)
          // Так как мы имеем дело с 2 независимыми zvals $d++ не
          // меняет $a и $b (как и ожидалось).

Как вы видите, при создание ссылки на zval c is_ref=0 и refcount>1 требует создания копии. Аналогично, при использовании zval с is_ref=1 и refcount>1 в контексте с передачей по значению потребуется операция копирования. По этой причине использование PHP-ссылок обычно замедляет код. Почти все функции в PHP используют семантику передачи по значению, поэтому они создают копию при получении zval со значением is_ref=1.

Выделение памяти и инициализация zval-ов

Теперь, когда вы ознакомились с основными идеями, лежащими в управлении памятью zval-ов, мы можем перейти к практике. Давайте начнем с выделения памяти для zval-а:

zval *zv_ptr;
ALLOC_ZVAL(zv_ptr);

Этот сниппет выделяет память под zval, но не инициализирует его члены. Ниже приведен пример макро, который может быть использован для создания постоянных zval-ов, которые не будут уничтожены в конце запроса:

zval *zv_ptr;
ALLOC_PERMANENT_ZVAL(zv_ptr);

Разница между этими двумя макросами в том, что первый использует emalloc(), а второй — malloc(). Важно знать, что попытка напрямую выделить память для zval-а не сработает:

/* Этот код НЕ РАБОТАЕТ */
zval *zv_ptr = emalloc(sizeof(zval));

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

typedef struct _zval_gc_info {
    zval z;
    union {
        gc_root_buffer       *buffered;
        struct _zval_gc_info *next;
    } u;
} zval_gc_info;

Макросы ALLOC_* создают zval_gc_info и инициализируют его дополнительный член, но затем его значение может быть прозрачно использовано так же как и zval (так как эта структура включает в себя zval как свой первый параметр).

После того как память под zval была выделена его нужно инициализировать. Для этой цели есть два макроса. Первый макро INIT_PZVAL, он устанавливает refcount=1 и is_ref=0 но оставляет значение неинициализированным:

zval *zv_ptr;
ALLOC_ZVAL(zv_ptr);
INIT_PZVAL(zv_ptr);
/* zv_ptr has garbage type+value here */

Второй макро INIT_ZVAL также устанавливает refcount=1 и is_ref=0, но в дополнение к этому устанавливает тип zval-а в IS_NULL:

zval *zv_ptr;
ALLOC_ZVAL(zv_ptr);
INIT_ZVAL(*zv_ptr);
/* zv_ptr has type=IS_NULL here */

INIT_PZVAL() может принимать указатель zval* (по этому буква P используется в его названии), а INIT_ZVAL() на вход принимает только zval. При передаче zval* во второй макро он должен быть сначала разыменован (dereferenced).

Так как это весьма распространенная задача: сначала выделить память, а затем инициализировать zval, для её решения существуют 2 макроса, которые объединяют оба шага:

zval *zv_ptr;
MAKE_STD_ZVAL(zv_ptr);
/* zv_ptr type и value здесь - мусор */

zval *zv_ptr;
ALLOC_INIT_ZVAL(zv_ptr);
/* zv_ptr type=IS_NULL  */

MAKE_STD_ZVAL() объединяет выделение памяти с INIT_PZVAL(), а ALLOC_INIT_ZVAL() объединяет его с INIT_ZVAL().

Управление счетчиком ссылок (refcount) и уничтожение zval

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

Z_REFCOUNT_P(zv_ptr)      /* Get refcount */
Z_ADDREF_P(zv_ptr)        /* Increment refcount */
Z_DELREF_P(zv_ptr)        /* Decrement refcount */
Z_SET_REFCOUNT(zv_ptr, 1) /* Установить refcount-у определенное значение (здесь 1) */

Как и с другими Z_ макросами здесь доступны варианты макросов без суффиксов и с суффиксами _P и _PP, которые принимают zval, zval* и zval** соответственно.

Чаще всего вы будуте использовать макро Z_ADDREF_P(). Вот небольшой пример:

zval *zv_ptr;
MAKE_STD_ZVAL(zv_ptr);
ZVAL_LONG(zv_ptr, 42);

add_index_zval(some_array, 0, zv_ptr);
add_assoc_zval(some_array, "num", zv_ptr);
Z_ADDREF_P(zv_ptr);

Этот код дважды вставляет число 42 в массив: сначала с индексом 0, затем с ключом num, таким образом, один zval будет использован в 2 местах. После того как с помощью MAKE_STD_ZVAL() выделена память и инициализирован новый zval его значение refcount равно 1. Чтобы использовать тот же zval в 2 местах нужно задать refcount-у значение 2, счетчик увеличивается при помощи Z_ADDREF_P().

Дополнительный макро Z_DELREF_P() используется реже, так как обычно недостаточно уменьшить значение счетчика ссылок, но и нужно проверить, что он не равен нули и, при необходимости, удалить zval и освободить память:

Z_DELREF_P(zv_ptr);
if (Z_REFCOUNT_P(zv_ptr) == 0) {
    zval_dtor(zv_ptr);
    efree(zv_ptr);
}

Макро zval_dtor() принимает на вход zval* и уничтожает его значение. Если это строка, то будет освобождена область памяти, занимаемая ею, если это массив — будет удалена хештаблица и освобождена память, если это объект или ресурс, то счетчик ссылок будет уменьшен (что также может привести к удалению и освобождению памяти).

Но вместо того чтобы писать код подобный приведенному выше и самостоятельно проверять значение refcount вам следует использовать макро zval_ptr_dtor():

zval_ptr_dtor(&zv_ptr);

Он принимает на вход zval** (по историческим причинам он также может принимать и zval*), уменьшает счетчик ссылок и проверяет должен ли этот zval быть уничтожен. Но в отличии от кода написанного вручную этот макро умеет находить циклические ссылки. Вот соответствующая часть его реализации:

static zend_always_inline void i_zval_ptr_dtor(zval *zval_ptr ZEND_FILE_LINE_DC TSRMLS_DC)
{
    if (!Z_DELREF_P(zval_ptr)) {
        ZEND_ASSERT(zval_ptr != &EG(uninitialized_zval));
        GC_REMOVE_ZVAL_FROM_BUFFER(zval_ptr);
        zval_dtor(zval_ptr);
        efree_rel(zval_ptr);
    } else {
        if (Z_REFCOUNT_P(zval_ptr) == 1) {
            Z_UNSET_ISREF_P(zval_ptr);
        }

        GC_ZVAL_CHECK_POSSIBLE_ROOT(zval_ptr);
    }
}

Z_DELREF_P() возвращает новое значение refcount после его уменьшения, по этому код !Z_DELREF_P(zval_ptr) аналогичен коду, который сначала удаляет ссылку Z_DELREF_P(zval_ptr), а затем проверяет значение счетчика Z_REFCOUNT_P(zval_ptr) == 0.

Кроме таких ожидаемых действий как zval_dtor() и efree() этот макро вызывает два GC_* макроса, которые обрабатывают циклические ссылки, и проверяет утверждение что не освобождена память переменной &EG(uninitialized_zval) (это магический zval используемый движком).

Кроме того, этот код устанавливает is_ref=0 в случае если осталась только одна ссылка на zval. Оставлять в таком случае is_ref=1 нельзя так как концепция &-ссылок в PHP имеет смысл только тогда, когда два и более владельца делят один zval.

Нексколько советов по использованию этих макросов. Вам никогда не следует использовать Z_DELREF_P() (он применим только тогда, когда вы можете гарантировать, что zval не должен быть уничтожен и не является корнем циклической ссылки). Вместо него, когда вы хотите уменьшить счетчик ссылок, вам следует использовать макро zval_ptr_dtor(). Макро zval_dtor() обычно используется с временными, находящимися в стэке zval-ами:

zval zv;
INIT_ZVAL(zv);

/* Do something with zv here */

zval_dtor(&zv);

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

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

Механизм копирования-при-записи позволяет избавиться от большого числа операция копирования zval-ов, но иногда такие операции нужны, например, если вы хотите изменить значение zval-а или переместить его в другое место хранилища.

PHP предоставляет большое число копирующих макросов для разных ситуаций, самый простой: ZVAL_COPY_VALUE(), он просто копирует значение и тип zval-а:

zval *zv_src;
MAKE_STD_ZVAL(zv_src);
ZVAL_STRING(zv_src, "test", 1);

zval *zv_dest;
ALLOC_ZVAL(zv_dest);
ZVAL_COPY_VALUE(zv_dest, zv_src);

В этом случае zv_dest будет иметь те же значение и тип, что и zv_src. Учтите, что "то же значение" означает, что оба zval-а будут иметь указатель на одну и ту же строку (char*), то есть если zv_src будет уничтожен, то область памяти, в которой хранится строка, будет очищена и zv_dest будет иметь висящий указатель на очищенную строку. Чтобы этого избежать должен быть вызыва копирующий конструктор zval_copy_ctor():

zval *zv_dest;
ALLOC_ZVAL(zv_dest);
ZVAL_COPY_VALUE(zv_dest, zv_src);
zval_copy_ctor(zv_dest);

zval_copy_ctor() сделает полную копию zval-а, i.e. то есть указатель на строку char* также будет скопирован. Если это массив, то его HashTable* будет скопирован, если это объект или ресурс, то их внутренние счетчики ссылок будут увеличены.

Мы пока не рассмотрели инициализацию счетчика ссылок и флага is_ref. Это может быть сделано с испольщованием макро INIT_PZVAL() или использованием MAKE_STD_ZVAL() вместо ALLOC_ZVAL(). Другая альтернатива — использование INIT_PZVAL_COPY() вместо ZVAL_COPY_VALUE(), который объединяет копирование и инициализацию refcount/is_ref:

zval *zv_dest;
ALLOC_ZVAL(zv_dest);
INIT_PZVAL_COPY(zv_dest, zv_src);
zval_copy_ctor(zv_dest);

Так как комбинация INIT_PZVAL_COPY() и zval_copy_ctor() очень частая, оба они объединены в макро MAKE_COPY_ZVAL():

zval *zv_dest;
ALLOC_ZVAL(zv_dest);
MAKE_COPY_ZVAL(&zv_src, zv_dest);

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

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

ZVAL_ZVAL(zv_dest, zv_src, copy, dtor)

Параметр copy определяет должен ли быть вызван конструктор zval_copy_ctor() для целевого zval-а, параметр dtor — должен ли быть вызван zval_ptr_dtor() для исходного zval-а. Давайте рассмотрим 4 возможных комбинации этих параметров и проанализируем поведение. Простейший пример это установка и copy и dtor в ноль:

ZVAL_ZVAL(zv_dest, zv_src, 0, 0);
/* equivalent to: */
ZVAL_COPY_VALUE(zv_dest, zv_src)

В этом случае ZVAL_ZVAL() ведет себя также как вызов ZVAL_COPY_VALUE(). Так что использование этого макро с аргументами 0, 0 не имеет никакого смысла. Гораздо полезнее вариант copy=1, dtor=0:

ZVAL_ZVAL(zv_dest, zv_src, 1, 0);
/* equivalent to: */
ZVAL_COPY_VALUE(zv_dest, zv_src);
zval_copy_ctor(&zv_src);

Это почти аналогично вызову MAKE_COPY_ZVAL(), за исключением того, что не вызывается INIT_PZVAL(). Это может быть полезно при копировании zval-а, который уже инициализирован (например, return_value). Установка dtor=1 добавит вызов zval_ptr_dtor():

ZVAL_ZVAL(zv_dest, zv_src, 1, 1);
/* equivalent to: */
ZVAL_COPY_VALUE(zv_dest, zv_src);
zval_copy_ctor(zv_dest);
zval_ptr_dtor(&zv_src);

Самый интересный случай это комбинация copy=0 и dtor=1:

ZVAL_ZVAL(zv_dest, zv_src, 0, 1);
/* equivalent to: */
ZVAL_COPY_VALUE(zv_dest, zv_src);
ZVAL_NULL(zv_src);
zval_ptr_dtor(&zv_src);

Этот код "перемещает" zval, то есть значение из zv_src перемещается в zv_dest без вызова копирующего конутруктора. Это следует делать в случае если zv_src имеет значение refcount=1, в этом случае zval будет уничтожен вызовом zval_ptr_dtor(). Если zval имеет большее значение refcount, тогда zval продолжить существовать со значением NULL.

Есит еще два макроса для копирования zval-ов: COPY_PZVAL_TO_ZVAL() и REPLACE_ZVAL_VALUE(). Оба они используются все реже и реже и не будут обсуждаться здесь.

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

Макросы описанные выше в основном используются когда вы хотите скопировать zval в другое место хранилища. Типичный пример — копирование значения в переменную return_value. Также существует второй набор макросов для "разделения zval-ов", которые используются в контексте копирования-при-записи. Их функциональность легко понять взглянув на исходник:

#define SEPARATE_ZVAL(ppzv)                     \
    do {                                        \
        if (Z_REFCOUNT_PP((ppzv)) > 1) {        \
            zval *new_zv;                       \
            Z_DELREF_PP(ppzv);                  \
            ALLOC_ZVAL(new_zv);                 \
            INIT_PZVAL_COPY(new_zv, *(ppzv));   \
            *(ppzv) = new_zv;                   \
            zval_copy_ctor(new_zv);             \
        }                                       \
    } while (0)

Если счетчик ссылок равен 1, то SEPARATE_ZVAL() не будет делать ничего. Если refcount больше единицы, то этот макро удалит одну ссылку из старого zval-а, скопирует её в новый zval и присвоит этот новый zval указателю *ppzv. Учтите, что макро принимает на вход zval** и модифицирует zval* на который он указывает.

Как это может пригодиться на практике? Предположим, вы хотите модифицировать элемент массива, например, $array[42]. Чтобы сделать это, сначала вам нужно извлечь из zval** указатель на хранящееся значение zval*. Учитывая подсчет ссылок вы не можете модифицировать его напрямую (так как он может быть совместно использован в нескольких местах), поэтому его сначала нужно разделить. При разделении в случае если refcount равен единице останется старое значение, иначе — будет осуществлено копирование. Во втором случае новый zval будет присвоен *ppzv, который в этом случае станет хранилищем массива.

Недостаточно просто сделать копию с помощью MAKE_COPY_ZVAL() так как скопированный zval не будет тем zval-ом, хранящимся в массиве.

Использование SEPARATE_ZVAL() напрямую для zval-ов у которых is_ref=1 не имеет смысла, в этом случае разделения не произойдет. Чтобы разобраться с этой ситуацией давайте посмотрим что PHP предлагает для обработки zval-ов с флагом is_ref:

Z_ISREF_P(zv_ptr)           /* Get if zval is reference */

Z_SET_ISREF_P(zv_ptr)       /* Set is_ref=1 */
Z_UNSET_ISREF_P(zv_ptr)     /* Set is_ref=0 */

Z_SET_ISREF_TO_P(zv_ptr, 1) /* Same as Z_SET_ISREF_P(zv_ptr) */
Z_SET_ISREF_TO_P(zv_ptr, 0) /* Same as Z_UNSET_ISREF_P(zv_ptr) */

Эти макросы также доступны в вариантах без суффикса и с суффиксами _P и _PP для обработки zval* и zval** соответственно. Кроме того, существует еще старый макро PZVAL_IS_REF(), являющийся синонимом Z_ISREF_P().

Используя их PHP предоставляет еще два варианта SEPARATE_ZVAL():

#define SEPARATE_ZVAL_IF_NOT_REF(ppzv)      \
    if (!PZVAL_IS_REF(*ppzv)) {             \
        SEPARATE_ZVAL(ppzv);                \
    }

#define SEPARATE_ZVAL_TO_MAKE_IS_REF(ppzv)  \
    if (!PZVAL_IS_REF(*ppzv)) {             \
        SEPARATE_ZVAL(ppzv);                \
        Z_SET_ISREF_PP((ppzv));             \
    }

Макро SEPARATE_ZVAL_IF_NOT_REF() обычно используется когда zval модифицируется в соответствии с копированием-при-записи. SEPARATE_ZVAL_TO_MAKE_IS_REF() используется если вы хотите сделать zval ссылкой (например при присвоении по ссылке или передачи ссылки как аргумент функции). Последний макро часто используется движком PHP и реже в коде расширений.

Есть еще один макро в семейсиве SEPARATE, который работает не так как другие:

#define SEPARATE_ARG_IF_REF(varptr) \
    if (PZVAL_IS_REF(varptr)) { \
        zval *original_var = varptr; \
        ALLOC_ZVAL(varptr); \
        INIT_PZVAL_COPY(varptr, original_var); \
        zval_copy_ctor(varptr); \
    } else { \
        Z_ADDREF_P(varptr); \
    }

Во-первых, этот макро принимает на вход zval*, а не zval**. По этому макро не сможет изменить zval, который он разделяет. Кроме того этот макро увеличивает refcount за вас, SEPARATE_ZVAL этого не делает.

И еще один дополнительный макро SEPARATE_ZVAL_IF_NO_REF(). На этот раз разделение происходит если zval является ссылкой. Он в основном используется для того, чтобы убедиться, что аргумент переданный в функцию это значение, а не ссылка.