Как сэкономить память, если нужно обработать большое количество объектов-моделей

в 10:38, , рубрики: iterator, php, оптимизация, потребление памяти, метки: , , ,

  Пост навеян статьей Сколько памяти потребляют объекты в PHP..., размышлениями над самописной ORM и книгой Мэтта Зандстра «PHP. Объекты, шаблоны и методики программирования» (ISBN 978-5-8459-1689-1).
  Мэтт в главе «Шаблоны баз данных» пишет о том, что если нужно создать несколько тысяч объектов из базы, то для экономии памяти, нужно решать эту задачу не «в лоб», а генерировать объекты по требованию, используя интерфейс Iterator.
  Первая мысль была: «Если мы достали 5000 записей из базы, значит мы хотим все их как-то обработать, и какая разница. сразу будут созданы объекты или по требованию?», но потом понял — если вся работа с каждым объектом происходит внутри цикла foreach или while( next() ), то создание объекта по требованию и автоматическое его уничтожение на следующем витке цикла даст существенную экономию памяти. На деле оказалось — очень существенную.

На чем тестировал: nginx + fast-cgi PHP 5.4.10 + APC.
Код:

// тестовый класс модели
Class Model {
    public $data = '';
    public function __construct($data)
    {
        $this->data = $data;    
    }
    
    public function process()
    {
        $this->data = strtoupper($this->data);
    }
}

// тестовые данные
$data =  str_repeat("jhsdfweurhjk234n", 500);

$start = microtime(1);
$collection = new Collection;

// добавляем в коллекцию 5000 записей 
for ( $i=0; $i<5000; ++$i ) {
    $collection->add($data);
}

// что-то делаем со всеми объектами коллекции 
foreach( $collection as $item ) {
    $item->process();
}
  
$end = microtime(1);
echo "Time: ". round( ($end-$start)*1000 , 2) . " ms <br>";
echo "Memory: ". round(memory_get_usage()/1024 , 2) . " kb <br>";

Класс Collection реализован был в двух вариантах:
в первом, объекты создаются сразу при добавлении данных в коллекцию

// коллекция объектов, реализует интерфейс Iterator
Class Collection implements Iterator
{
    protected $position = 0;
    protected $items = array();
    
    public function add( $data )
    {
        // заранее создаем объекты
        $this->items[] = new Model($data); 
    }
    
    function rewind() {
        $this->position = 0;
    }

    function current() {
        return $this->items[$this->position];
    }

    function key() {
        return $this->position;
    }

    function next() {
        ++$this->position;
    }

    function valid() {
        return isset($this->items[$this->position]);
    }
}

Во втором, объекты создаются только по-требованию, а в коллекции хранятся только данные

Class Collection implements Iterator
{
    protected $position = 0;
    protected $items = array();
    
    public function add( $data )
    {
        // храним только данные
        $this->items[] = $data; 
    }
    
    function rewind() {
        $this->position = 0;
    }

    function current() {
        // создаем объект по требованию
        return new Model($this->items[$this->position]);
    }

    function key() {
        return $this->position;
    }

    function next() {
        ++$this->position;
    }

    function valid() {
        return isset($this->items[$this->position]);
    }
}

Результаты тестирования:

Способ Время, мс Расход памяти, кб
Создание всех объектов сразу 1389±10 40279,82
Создание объектов по-требованию 1344±10 415,28

Экономия памяти получилась в буквальном смысле в 100 раз.

Популярная ORM Doctrine 1.* уже содержит гидратор Doctrine_Core::HYDRATE_ON_DEMAND, который вроде как реализует ту же логику docs.doctrine-project.org/projects/doctrine1/en/latest/en/manual/data-hydrators.html#on-demand

Автор: bardex

Источник

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


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