Изучаем PHP изнутри. Zval

в 6:54, , рубрики: php, Веб-разработка, книга, Программирование, метки: ,

Эта статья базируется на главе Zvals книги PHP Internals Book, переводом которой на русский язык я сейчас занимаюсь [1]. Книга ориентирована в первую очередь на C-программистов, желающих писать свои расширения для PHP, но, я уверен, что она окажется полезной и для PHP-разработчиков, так как описывает внутреннюю логику работы интерпретатора. В статье я оставил только базовую теорию, которая должна быть понятна всем разработчикам (даже не знакомым с PHP или C). За более полным изложением материала обратитесь к книге.

Задачка для привлечения внимания. Каким будет результат выполнения следующего кода?

$obj1 = new StdClass();
$obj2 = new StdClass();

$obj1->value = 1;
$obj2->value = 1;

function f1($o) {
  $o = 100;
}

function f2($o) {
  $o->value = 100;
}

f1($obj1);
f2($obj2);

var_dump($obj1);
var_dump($obj2);
Ответ
object(stdClass)#1 (1) { [«value»]=> int(1) }
object(stdClass)#2 (1) { [«value»]=> int(100) }

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

Базовая структура

Базовой структурой данных в PHP является zval (сокращение от «Zend value»). Каждый zval хранит в себе несколько полей, два из которых — значение и тип этого значения. Это необходимо потому что PHP — это язык с динамической типизацией и по этому тип переменных известен только во время выполнения программы, а не во время компиляции. Кроме того, тип переменной может быть изменен в течении жизни zval, то есть zval ранее хранимый как целое число позднее может содержать строку.

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

zvalue_value

Фактическое значение переменной хранится в типе данных union («объединение», в дальнейшем я буду использовать термины union или юнион), который определен следующим образом:

typedef union _zvalue_value {
    long lval;
    double dval;
    struct {
        char *val;
        int len;
    } str;
    HashTable *ht;
    zend_object_value obj;
} zvalue_value;

Небольшое пояснение для тех кто не знаком с концепцией union-ов. Union определяет несколько членов-данных различных типов, но в каждый момент времени может использоваться только одно значение из определенных в юнионе. Например, если члену данных value.lval было присвоено значение, то для доступа к данным вы можете использовать только value.lval, доступ к другим членам данных недопустим и может приводить к непредсказуемому поведению программы. Причина этого в том, что юнионы хранят данные всех своих членов в одной области памяти и интерпретируют значение по разному исходя из имени, к которому вы обращаетесь. Размер памяти, выделяемой для юниона, соответствует размеру самого большого его члена-данных.

При работе с zval-ами используется специальная метка (type tag), которая позволяет определить какой тип данных хранится в юнионе в данный момент. Прежде чем обратиться к API давайте посмотрим какие типы данных поддерживаются в PHP и как они хранятся.

Простейший тип данных — IS_NULL: он не должен хранить какое-либо значение, так как это просто null.

Для хранения чисел PHP представляет 2 типа: IS_LONG и IS_DOUBLE, которые используют члены long lval и double dval соответственно. Первый используется для хранения целых чисел, второй — для чисел с плавающей точкой.

Есть несеколько вещей, которые вам следует знать о типе данных long. Во-первых, это signed integer, то есть он может содержать положительные и отрицательные значения, но этот тип данных не подходит для побитовых операций. Во-вторых, long имеет разные размеры на разных платформах: на 32-битных системах он имеет размер 32 бита или 4 байта, но на 64-битных системах он может иметь размер как 4, так и 8 байт. В Unix-системах он обычно имеет размер в 8 байт, в то время как в 64-битных версиях Windows использует только 4 байта.

По этой причине вы не должны полагаться на конкретное значение типа long. Минимальное и максимальное значения, которые могут быть сохранены в типе данных long доступны в константах LONG_MIN и LONG_MAX и размер этого типа может быть определен с использованием макро SIZEOF_LONG (в отличии от sizeof(long) этот макро может быть использован и в #if директивах).

Тип данных double предназначен для хранения чисел с плавающей точкой и, обычно, следуя спецификации IEEE-754, он имеет размер в 8 байт. Детали этого формата не будут обсуждаться здесь, но вам как минимум следует знать, что этот тип имеет ограниченную точность и часто хранит не точно то значение, на которое вы рассчитываете.

Булевы переменные используют флаг IS_BOOL и хранятся в поле long val как значения 0 (false) и 1 (true). Так как этот тип использут только 2 значения, то, теоретически, достаточно было использовать тип меньшего размера (например zend_bool), но так как zvalue_value — это юнион и под него и так выделен объем памяти соответствующий самому большому члену данных, то применение более компактной переменной для булевых значений не приведет к экономии памяти. По этому lval повторно использован в этом случае.

Строки (IS_STRING) хранятся в структуре struct {char *val; int len; } str;, то есть строка хранится как указатель на строку char * и целочисленная длина строки int. Строки в PHP должны явно хранить свою длину для того чтобы иметь возможность содержать NUL байты () и быть бинарно безопасными (binary safe). Но несмотря на это, строки используемые в PHP все равно заканчиваются нулевым байтом (NUL-terminated), чтобы обеспечить совместимость с библиотечными функциями, которые не принимают аргумент с длиной строки, а ожидают найти нулевой байт в конце строки. Конечно, в таких случаях строки больше не могут быть бинарно безопасными и будут обрезаны до первого вхождения нулевого байта. Например, много функций связанных с файловой системой и большинство строковых функций из libc ведут себя подобным образом.

Длина строки измеряется в байтах (не числом Unicode-символов) и не должно включать нулевой байт, то есть длина строки foo равна 3, несмотря на то, что для её хранения используется 4 байта. Если вы оперделяете длину строки с использованием sizeof вам нужно вычитать единицу: strlen("foo") == sizeof("foo") - 1.

Очень важно понимать: длина строки хранится в типе int, а не в long или каком-то другом похожем типе. Это исторический артефакт, который ограничивает длину строки 2147483647 байтами (2 гигабайта). Строки большего размера будут причиной переполнения (что сделает их длину отрицательной).

Оставшиеся три типа будут упомянуты лишь поверхностно.

Массивы используют метку IS_ARRAY и хранятся в члене-данных HashTable *ht. Как работает структура данных HashTable рассмотрено в другой статье.

Объекты (IS_OBJECT) исползуют член-данных zend_object_value obj, который состоит из «object handle» (целочисленный ID, используемый для поиска реальных данных) и набора «object handlers», которые определяют поведение объекта. Система классов и объектов в PHP будет описана в главе «Классы и объекты».

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

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

Type tag Storage location
IS_NULL none
IS_BOOL long lval
IS_LONG long lval
IS_DOUBLE double dval
IS_STRING struct { char *val; int len; } str
IS_ARRAY HashTable *ht
IS_OBJECT zend_object_value obj
IS_RESOURCE long lval

zval

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

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

Как уже упоминалось, zval содержит члены для хренения значения и его типа. Значение хранится в юнионе zvalue_value, который описан выше. Тип хранится в zend_uchar type. Кроме того эта структура содержит 2 дополнительных свойства, имена которых заканчиваются на __gc, которые используются механизмом сборки мусора. Подробнее эти свойства рассмотрены в следующем разделе.

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

Структура данных 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 уничтожен, так как 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.

Заключение

В этой статье я привел выжимку главы Zvals книги PHP Internals Book. Я постарался оставить только тот материал, который будет полезен PHP-разработчикам и вырезал много текста, связанного с разработкой расширений (иначе объем статьи вырос бы раза в 3). Если вам интересно глубже изучить вопрос разработки расширений для PHP вы можете обратиться к книге или моему переводу. На данный момент переведена только глава Zvals, но я продолжаю работу. В ближайшее время займусь интереснейшими главами про хештаблицы и классы.


[1] Перевод книги делается с разрешения авторов, но он является неофициальным. Почитать мой перевод можно тут: romka.gitbooks.io/php-internals-book-ru, помочь переводить тут: github.com/romka/phpinternalsbook-ru

Автор: rrromka

Источник

  1. dardarbl4:

    очень интересно, спасибо за статью. Редкий материал было интересно и познавательно прочитать

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js