Как устроены переменные в PHP

в 21:07, , рубрики: internals, php, variables, метки: , ,

Вроде простой вопрос, даже не понятно что на него ответить, правда?
Мы все знаем как создать переменную, как получить значение переменной, как взять ссылку на переменную в конце концов.
Но как они работают изнутри?
Что происходит в интерпретаторе, когда вы изменяете значение переменной? Или когда удаляете ее?
Как реализованы типы переменных?

В этой статье я постараюсь раскрыть именно эти темы.

Abstract

Переменные в PHP выражены в виде неких контейнеров, которые хранят в себе тип переменной, значение, кол-во ссылающихся переменных на этот контейнер, и флаг — является ли эта переменная ссылочной.

Как устроены переменные в PHP

Отступление про структуры и указатели

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

my_super_struct super_struct_instance;

Указатели — это как переменные-ссылки, только их значение — это адрес в памяти. На самом деле, это ссылки как указатели, только они ведут себя как разыменованные указатели. Лучше показать на коде:

// создали указатель foo, который будет указывать на переменную типа int
int *foo;
// создали переменную типа int
int bar = 3;

// взяли ссылку на переменную bar и присвоили ее указателю.
// теперь foo хранит адрес ячейки памяти, в которой хранится bar
foo = &bar;

// с помощью астериска мы разыменовываем указатель (берем значение по его адресу)
// и инкрементируем значение
(*foo)++;

// а так мы инкрементируем сам указатель, то есть после этой
// операции указатель будет смотреть на другое значение
foo++;

Как устроены переменные в PHP

Контейнеры

Контейнером служит структура под названием zval, она выглядит так:

struct zval {
    zvalue_value value;
    zend_uchar type; // можно предположить, что это обычный char
    zend_uchar is_ref;
    zend_ushort refcount;
};

Как мы видим, здесь есть значение, тип, флаг и кол-во ссылающихся переменных.
Здесь есть такие типы, как:

  • LONG
  • BOOL
  • DOUBLE
  • STRING
  • ARRAY
  • OBJECT
  • RESOURCE
  • NULL

zvalue_value — это union. Union — это такой тип, в котором можно объявить несколько членов разных типов, но использоваться в итоге будет только один, вот как он дефайнится:


typedef union _zvalue_value {
        long lval; // integer
        double dval; // float
        struct {
                char *val;
                int len;
        } str; // string
        HashTable *ht; // array
        zend_object obj; // object
} zvalue_value;

В итоге, когда вы будете создавать переменную этого типа, она займет в памяти ровно столько, сколько занимает самый тяжелый элемент юниона.
Как устроены переменные в PHP

Зачем столько лишенго?

Теперь разберем — зачем тут, например, какой-то refcount?
А очень просто: когда вы присваиваете переменной значение другой переменной, то они обе ссылаются на один zval, а refcount инкрементируется.
Как устроены переменные в PHP
(оригинал с собачкой тут)

Теперь, если вы захотите изменить значение одной из этих переменных, то PHP, увидя refcount больше 1, скопирует этот zval, сделает изменения там, и ваша переменная будет указывать уже на новый zval.
Если это немного формализовать, то это будет выглядеть примерно так:

PHP Под капотом
$foo = "asd";
$bar = $foo;
bar,foo: {
    type: string,
    value:
        str:
            val: "asd"
            len: 3
    is_ref: 0
    refcount: 2
}
$bar .= "q";
foo: {
    type: string,
    value:
        str:
            val: "asd"
            len: 3
    is_ref: 0
    refcount: 1
}
bar: {
    type: string,
    value:
        str:
            val: "asdq"
            len: 4
    is_ref: 0
    refcount: 1
}

Эта техника называется copy on write и она позволяет неплохо снизить потребление памяти.
Также, refcount нужен сборщику мусора, который удаляет из памяти все zval-ы, у которых refcount = 0.

А что делать с ссылками и зачем вообще этот is_ref?

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

PHP Под капотом
$foo = "asd";
$bar = $foo;
bar,foo: {
    type: string,
    value:
        str:
            val: "asd"
            len: 3
    is_ref: 0
    refcount: 2
}
$zxc = &$foo;
zxc,foo: {
    type: string,
    value:
        str:
            val: "asd"
            len: 3
    is_ref: 1
    refcount: 2
}
bar: { // переменная bar была выделена в отдельный zval
    type: string,
    value:
        str:
            val: "asd"
            len: 3
    is_ref: 0
    refcount: 1
}
$qwe = $foo;
zxc,foo: {
    type: string,
    value:
        str:
            val: "asd"
            len: 3
    is_ref: 1
    refcount: 2
}
bar: {
    type: string,
    value:
        str:
            val: "asd"
            len: 3
    is_ref: 0
    refcount: 1
}
qwe: { // эта переменная тоже была выделена в отдельный zval
    type: string,
    value:
        str:
            val: "asd"
            len: 3
    is_ref: 0
    refcount: 1
}

Конечно, если вы возьмете еще одну ссылку от foo, то refcount zval-а, на который ссылается foo, увеличится на один.

Пожалуй на этом (пока?) все, в следующей части поговорим о массивах.

PS не знаю кто как воспримет эти картинки, мне показалось это будет забавно :) к сожалению сканера у меня нет

Автор: nikita2206

Источник


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


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