Подписка

Прожорливые массивы

Отсутсвие жесткой типизации в PHP, на первый взгляд упрощает разработку - не нужно задавать типы переменных, заботиться о конвертации - все делает интепритатор. Конечно же за простоту и удобство прийдется платить и чем важнее производительность тем больше. Ниже речь пойдет о любимых массивах, быстрых и простых на первый взгляд, но прожорливых к памяти при больших объемах.

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

Наверняка это удобно работать с универсальным типом данных, для которого, как и везде в PHP, не нужно указывать типизацию - в одну корзину можно ложить и объекты, и строки, да в общем что угодно.

А как же интерпритатор работает с нетипизированными переменными?

Интерпритатор хранит каждую переменную в виде структуры Zval, содержащую поля:

  • zvalue_value value - значение переменной
  • zend_uint refcount__gc - количество ссылок на переменную (если оно равно 0 - уборщик мусора удаляет переменную и освобождает память)
  • zend_uchar type - тип переменной
  • zend_uchar is_ref__gc - флаг определяющий, что переменная является ссылкой

Нас интересует поле value, которое содержит значение переменной представленое в виде объединения, содержащего поля для всех типов данных:

typedef union _zvalue_value {
    long lval — численные + булевые и ресурсы (их идентификаторы)
    double dval — double 
    struct { char *val; int len; } str - строка
    HashTable *ht — массив
    zend_object_value obj — объект и null
}

В зависимости от типа присваемых данных, значение записывается в нужное поле (например а=1 запишется в lval).

Давайте взвесим массив

Измерим, сколько памяти занимает один целочисленный элемент массива. Сначала взвесим пустой массив:

$p1 = memory_get_usage();
$a = array();
echo (memory_get_usage() - $p1);

Теперь один элемент:

$p1 = memory_get_usage();
$a = array(1);
echo (memory_get_usage() - $p1);

Ну и чтобы наверняка - два элемента:

$p1 = memory_get_usage();
$a = array(1,2);
echo (memory_get_usage() - $p1);

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

136 * 10000 = 1360000 = ~1.4Мб

Ничего так получилось, правда? На одну только небольшую сетку полтора мегабайта. А давайте попробуем вместо переменной статуса, использовать простой объект:

class cellStatus {
    protected $status = 0;

    public function getStatus() { 
        return $this->status;
    }

    public function setStatus($values) { 
        $this->status = $value;
    }
}

У меня получилось 5.6Мб! Представьте что будет, если сделать поле 1000х1000, даже с целочисленными статусами.

Плохи наши дела

Вот и аукнулась нестрогая типизация - переменная, которая при строгой весила бы 8 байт, весит 136, из-за чего сильно увеличивается потребление памяти.

Так что же делать, чтобы избежать огромных затрат? Совет "не использовать массивы" наверно прозвучит глупо и всеравно не поможет, поэтому советую использовать структуры данных SPL. Например, SPL массив из 10 элементов:

$a = new SplFixedArray(10);

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

В тему:

Если пост понравился - нажмите на +1 - мне будет приятно.

@kkooler

@kkooler

Занимаюсь разработкой высоконагруженных проектов и распределенных систем на PHP.
В свободное время разрабатываю нано-проекты:

Следить за блогом

RSS канал Twitter