28.12 2010

PHP

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

Первое, в чем стоит разобраться, это способ хранения переменных. Так как PHP написан на языке С, который строго типизирован, то перед разработчиками встала задача разработать такой тип данных, который бы удовлетворил ВСЕ типы PHP. А их, напомню, восемь:

  • NULL
  • Long int
  • Double
  • Boolean
  • String
  • Array
  • Object
  • Resource

Все эти типы в C представляются с помощью единого типа zval, структура которого представлена ниже:

typedef struct _zval_struct {
    /* Variable information */
    zvalue_value value;        /* value */
    zend_uint refcount__gc;
    zend_uchar type;           /* active type */
    zend_uchar is_ref__gc;
} zval;

Пока не стоит обращать внимание на refcount__gc и is_ref__gc. Они используются сборщиком мусора, о котором мы поговорим позже. Наибольший же интерес вызывают переменные value и type. Из предназначение легко угадывается из названия. Тип представляет собой целочисленное число:

#define IS_NULL		0
#define IS_LONG		1
#define IS_DOUBLE	2
#define IS_BOOL		3
#define IS_ARRAY	4
#define IS_OBJECT	5
#define IS_STRING	6
#define IS_RESOURCE	7

Переменная value более интересна. Она представляет из себя объединение:

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

Как можно заметить, это объединение содержит в себе описание типов long, double, string, array, object. Естественно возникает вопрос, а как же остальные типы?

  • Тип boolean записывается в переменную lval.
  • Null не имеет значения (вернее имеет всегда одно), его достаточно описать в type
  • Resource сохраняет указатель на свою структуру в lval

У разработчиков PHP на мой взгляд получилось красиво решить поставленную проблему, однако, как следствие, мы имеет громадный оверхед памяти. На 32 битной системе, для хранения одной булевой переменной понадобиться 16! байт памяти и это без учета имени переменной, которое так же будет храниться.

Хранение массивов (HashTable)

PHP  не делает различий между нумерованными массивами и словарями (хеш-массивами), с точки зрения программиста. Однако разработчики позаботились, чтобы нумерованные списки обрабатывались "особым" способом, повышая тем самым производительность. В итоге получилась довольно монстрообразная структура, представляющая собой смесь обычного массива и двусвязного списка:

typedef struct bucket {
    ulong h;	/* Used for numeric indexing */
    uint nKeyLength;
    void *pData;
    void *pDataPtr;
    struct bucket *pListNext;
    struct bucket *pListLast;
    struct bucket *pNext;
    struct bucket *pLast;
    char arKey[1]; /* Must be last element */
} Bucket;

typedef struct _hashtable {
    uint nTableSize;
    uint nTableMask;
    uint nNumOfElements;
    ulong nNextFreeElement;
    Bucket *pInternalPointer;	/* Used for element traversal */
    Bucket *pListHead;	/* First element of array */
    Bucket *pListTail;	             /* Last element */
    Bucket **arBuckets;
    dtor_func_t pDestructor;
    zend_bool persistent;
    unsigned char nApplyCount;
    zend_bool bApplyProtection;
#if ZEND_DEBUG
    int inconsistent;
#endif
} HashTable;

Размер HashTable - 44 байта, а Bucket - 36 байт (на 32 битной системе), и как результат массив:

array(null);

имеет размер 96 байт. Следует это учитывать, если вашему проекту понадобится обработать большой поток данных.

К слову, именно в  HashTable и хранятся имена переменных, локальных и глобальных

Объекты

В целом, объект представляет из себя набор переменных (zval), и функций, а также описания класса. Классы представляются в памяти с помощью структуры данных zend_class_entry

typedef struct _zend_class_entry {
    char type;
    char *name;
    zend_uint name_length;
    struct _zend_class_entry *parent;
    int refcount;
    zend_bool constants_updated;
    zend_uint ce_flags;

    HashTable function_table;
    HashTable default_properties;
    HashTable properties_info;
    HashTable default_static_members;
    HashTable *static_members;
    HashTable constants_table;
    const struct _zend_function_entry *builtin_functions;

    union _zend_function *constructor;
    union _zend_function *destructor;
    union _zend_function *clone;
    union _zend_function *__get;
    union _zend_function *__set;
    union _zend_function *__unset;
    union _zend_function *__isset;
    union _zend_function *__call;
    union _zend_function *__callstatic;
    union _zend_function *__tostring;
    union _zend_function *serialize_func;
    union _zend_function *unserialize_func;

    zend_class_iterator_funcs iterator_funcs;

    /* handlers */
    zend_object_value (*create_object)(zend_class_entry *class_type TSRMLS_DC);
    zend_object_iterator *(*get_iterator)(zend_class_entry *ce, zval *object, int by_ref TSRMLS_DC);
    int (*interface_gets_implemented)(zend_class_entry *iface, zend_class_entry *class_type TSRMLS_DC); /* a class implements this interface */
    union _zend_function *(*get_static_method)(zend_class_entry *ce, char* method, int method_len TSRMLS_DC);

    /* serializer callbacks */
    int (*serialize)(zval *object, unsigned char **buffer, zend_uint *buf_len, zend_serialize_data *data TSRMLS_DC);
    int (*unserialize)(zval **object, zend_class_entry *ce, const unsigned char *buf, zend_uint buf_len, zend_unserialize_data *data TSRMLS_DC);

    zend_class_entry **interfaces;
    zend_uint num_interfaces;

    char *filename;
    zend_uint line_start;
    zend_uint line_end;
    char *doc_comment;
    zend_uint doc_comment_len;

    struct _zend_module_entry *module;
} zend_class_entry;

А объекты с помощью zend_object_value

typedef unsigned int zend_object_handle;
typedef struct _zend_object_handlers zend_object_handlers;

typedef struct _zend_object_value {
    zend_object_handle handle;
    zend_object_handlers *handlers;
} zend_object_value;

struct _zend_object_handlers {
    /* general object functions */
    zend_object_add_ref_t					add_ref;
    zend_object_del_ref_t					del_ref;
    zend_object_clone_obj_t					clone_obj;
    /* individual object functions */
    zend_object_read_property_t				read_property;
    zend_object_write_property_t			write_property;
    zend_object_read_dimension_t			read_dimension;
    zend_object_write_dimension_t			write_dimension;
    zend_object_get_property_ptr_ptr_t		get_property_ptr_ptr;
    zend_object_get_t						get;
    zend_object_set_t						set;
    zend_object_has_property_t				has_property;
    zend_object_unset_property_t			unset_property;
    zend_object_has_dimension_t				has_dimension;
    zend_object_unset_dimension_t			unset_dimension;
    zend_object_get_properties_t			get_properties;
    zend_object_get_method_t				get_method;
    zend_object_call_method_t				call_method;
    zend_object_get_constructor_t			get_constructor;
    zend_object_get_class_entry_t			get_class_entry;
    zend_object_get_class_name_t			get_class_name;
    zend_object_compare_t					compare_objects;
    zend_object_cast_t						cast_object;
    zend_object_count_elements_t			count_elements;
    zend_object_get_debug_info_t			get_debug_info;
    zend_object_get_closure_t				get_closure;
};

Все типы zend_object_*_t это указатели на функции

Приведение типов

Приведение типов происходит в соответствии с принятыми соглашениями, про которые можно почитать в документации http://www.php.net/manual/en/language.types.type-juggling.php. Так результатом выполнения:

var_dump("1"+2+3.0);

будет float(6)

Для упрощения процесса конвертирования, в PHP существует группа функций:

ZEND_API void convert_scalar_to_number(zval *op TSRMLS_DC);
ZEND_API void _convert_to_string(zval *op ZEND_FILE_LINE_DC);
ZEND_API void convert_to_long(zval *op);
ZEND_API void convert_to_double(zval *op);
ZEND_API void convert_to_long_base(zval *op, int base);
ZEND_API void convert_to_null(zval *op);
ZEND_API void convert_to_boolean(zval *op);
ZEND_API void convert_to_array(zval *op);
ZEND_API void convert_to_object(zval *op);
#define convert_to_string(op) if ((op)->type != IS_STRING) { _convert_to_string((op) ZEND_FILE_LINE_CC); }

Следует отметить, что изменения типа производится НЕ передаваемой переменной, а ее копии, чтобы избежать ситуаций, как ниже:

$s = "12A";
var_dump($s); // string(3) "12A"
$a = $s + 1;
var_dump($s); // int(12) - fail</pre>

refcount__gc и is_ref__gc

Сначала я приведу небольшой кусок кода:

print memory_get_usage(true); // 524288
$s = str_repeat("s", 1000000);
print memory_get_usage(true); // 1572864
$a = $s;
print memory_get_usage(true); // 1572864
$s[0] = 0;
print memory_get_usage(true); // 2621440

Давайте попробуем разобраться, что же здесь происходит. На первой строке выводит начальное состояние памяти, на второй создаем строку длинной 1000000 символов, делаем замер памяти и видим ее закономерное увеличение. Затем мы копируем эту строку в другую переменную, делаем замер и... ничего не изменилось. Почему? И почему при изменении всего одного символа мы видим то, чего ожидали увидеть двумя строками выше? Здесь вступает в силу небольшая оптимизация от разработчиков PHP, которые решили не копировать переменную до тех пор, пока в этом не возникнет необходимость. Как же это работает?

Для начала рассмотрим такой пример:

$a = 15;
$b = &$a

В итоге этой операции у нас появляется две переменные, ссылающиеся на одну область памяти. Для того чтобы сборщик мусора и иные функции знали об этом и корректно оперировали с такой переменной, в is_ref__gc записывается 1, а в refcount__gc - 2 (так как 2 переменные ссылается на эту область памяти).

Теперь, если мы сделаем

unset($a);

refcount__gc станет 1, и область памяти останется живой, когда refcount__gc достигнет 0, область очиститься.

Вернемся же к нашему примеру, что же там происходит? При копировании переменной происходит проверка, если is_ref__gc = 0, то refcount__gc увеличивается на 1, и эта переменная начинает ссылаться на ту же область памяти. При изменение же, любой из переменных, происходит "разделение" и создается копия с параметрами is_ref__gc = 0 и refcount__gc = 1.

Вы можете сами в этом убедиться сделав так:

print memory_get_usage(true); // 524288
$s = str_repeat("s", 1000000);
print memory_get_usage(true); // 1572864
$a = &$a; // Magic ;)
$a = $s;
print memory_get_usage(true); // 2621440
$s[0] = 0;
print memory_get_usage(true); // 2621440

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

comments powered by Disqus