Ресурсоемкие операции в php: Память или скорость

Проблема: нехватка памяти при выполнении php-скрипта.
Методы решения: только рефакторинг (и никаких set_memory_limit).

И сразу же о результате, который меня поразил:
26176 kb -> 12224 Kb
Спасибо Scratch!
Эта статья — репост из его блога.

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

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

<?php
    // This script runs more than 30 secs.
    set_time_limit(0);
    $test = array();
    for ($i=0; $i<40000; $i++) {
      $test[$i] = array();
      for ($k=0; $k<5; $k++) {
        $test[$i][$k] = array();
        for ($j=0; $j<5; $j++) {
          $test[$i][$k][$j] = $i+$k+$j;
        }
      }
    }
    // So, array will contain
    // 40000 * 5 * 5 integers (1000000 elements) 

    // And sleep to have time to
    // see memory usage in Task Manager
    sleep(100);
?>

Как вы думаете, сколько будет занимать полученный массив $test() в памяти?

На скришноше моего TaskManager показано, сколько занимает в памяти Apache (PHP установлен как модуль) в момент выполнения команды sleep().

High loaded script

Думаю, комментарии здесь излишни. Скрипт использует примерно в 200 раз больше памяти, чем можно было предположить.

Этот перерасход не представляет ничего особо опасного до тех пор, пока объем памяти скрипта не начинает превышать объем оперативной памяти того компьютера, на котором он выполняется. В этом случае в силу вступает другой момент — скрипт начинает дико тормозить, в основном из-за того, что windows начинает выделять виртуальную память в файле подкачки. И эта операция вызывает очень большое замедление работы (причем — не только текущего скрипта). Все другие скрипты, сервисы и так далее тоже начинают тормозить, и это можно сравнивать с эффектом DoS (Чем это торможение, в принципе, и является).

Теперь я внесу небольшое изменение в скрипт, а именно — внутри внешнего цикла, после заполнения элемента $test[$i] я его сериализую.

<?php
    // This script runs more than 30 secs.
    set_time_limit(0);
    $test = array();
    for ($i=0; $i&lt;40000; $i++) {
      $test[$i] = array();
      for ($k=0; $k&lt;5; $k++) {
        $test[$i][$k] = array();
        for ($j=0; $j&lt;5; $j++) {
          $test[$i][$k][$j] = $i+$k+$j;
        }
      }
      $test[$i] = serialize($test[$i]); 

    }
    // So, array will contain
    // 40000 * 5 * 5 integers (1000000 elements) 

    // And sleep to have time to
    // see memory usage in Task Manager
    sleep(100); 

?>

Смотрим на полученный скриншот

Low loaded script

Как видно, объем занимаемой памяти уменьшился примерно в 4 раза и, что самое главное, этот объем памяти не требует операции swap. Это существенно ускоряет работу скрипта (хотя операция serialize должна была его замедлить).

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

Вы можете спросить, почему эот происходит, и почему PHP использует так много памяти?
Я мог предположить только одно (и подозреваю, что это действительно так).
Дело в том, что массивы в PHP — на самом деле хеши. Для каждого массива динамически создается таблица, по которой находится адрес ключа. Такие таблицы — это вполне обычные практики, они используются для ускорения доступа к элементам.
Кроме того, при использовании таких таблиц, опять же для ускорения работы с памятью, память для таблиц выделяется блочно — то есть не для записи каждого элемента, а для записи, например, 100 элементов сразу. Тогда, при превышении объема таблицы, выделяется память еще на 100 элементов и так далее…

Проблема в том, что для маленьких таблиц память все равно выделяется на эти же 100 элементов. Таким образом, при создании массива с одним элементом, создается также таблица ключей, размером в 100 элементов. И подобная схема очень сильно расходует память.
Использование же serizlize переводит массив в тесктовую форму, для которой такие дополнительные скрытые таблицы не создаются.
Именно поэтому расходуемая память меньше.

Автор: Scratch, источник: Память против скорости, или почему иногда стоит использовать serialize

Метод, это нечто!
Мой скрипт умирал на середине кода с ошибкой переполнения памяти. Не помогли ни mysql_unbuffered_query, ни mysql_free_result, ни unset.
Но после простого serialize элементов одного из двумерных массивов, повторюсь, результат просто поразил:
26176 kb -> 12224 Kb — это для проблемного места (сохранение двумерного массива с 15К записями)
Для места с unserialize расход памяти:
В начале цикла с unserialize — 15 747 816 байт
В конце цикла 15 921 344 байт

Полезно(1)Бесполезно(0)

Добавить комментарий