Память против скорости, или почему иногда стоит использовать serialize
January 31, 2008 by 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().
Думаю, комментарии здесь излишни. Скрипт использует примерно в 200 раз больше памяти, чем можно было предположить.
Этот перерасход не представляет ничего особо опасного до тех пор, пока объем памяти скрипта не начинает превышать объем оперативной памяти того компьютера, на котором он выполняется. В этом случае в силу вступает другой момент — скрипт начинает дико тормозить, в основном из-за того, что windows начинает выделять виртуальную память в файле подкачки. И эта операция вызывает очень большое замедление работы (причем — не только текущего скрипта). Все другие скрипты, сервисы и так далее тоже начинают тормозить, и это можно сравнивать с эффектом DoS (Чем это торможение, в принципе, и является).
Теперь я внесу небольшое изменение в скрипт, а именно — внутри внешнего цикла, после заполнения элемента $test[$i] я его сериализую.
< ?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; } } $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); ?>
Смотрим на полученный скриншот
Как видно, объем занимаемой памяти уменьшился примерно в 4 раза и, что самое главное, этот объем памяти не требует операции swap. Это существенно ускоряет работу скрипта (хотя операция serialize должна была его замедлить).
Именно с этой проблемой я столкнулся при решении одной из задач обработки больших объемов данных на PHP. И решение было именно таким.
Вы можете спросить, почему эот происходит, и почему PHP использует так много памяти?
Я мог предположить только одно (и подозреваю, что это действительно так).
Дело в том, что массивы в PHP — на самом деле хеши. Для каждого массива динамически создается таблица, по которой находится адрес ключа. Такие таблицы — это вполне обычные практики, они используются для ускорения доступа к элементам.
Кроме того, при использовании таких таблиц, опять же для ускорения работы с памятью, память для таблиц выделяется блочно — то есть не для записи каждого элемента, а для записи, например, 100 элементов сразу. Тогда, при превышении объема таблицы, выделяется память еще на 100 элементов и так далее…
Проблема в том, что для маленьких таблиц память все равно выделяется на эти же 100 элементов. Таким образом, при создании массива с одним элементом, создается также таблица ключей, размером в 100 элементов. И подобная схема очень сильно расходует память.
Использование же serizlize переводит массив в тесктовую форму, для которой такие дополнительные скрытые таблицы не создаются.
Именно поэтому расходуемая память меньше.
Filed under: Deep Internals |
Интересно…
4-х кратная экономия памяти это очень весомый аргумент.
Правда, хотелось бы увидеть более подробное тестирование. Например, с использованием обратного преобразования unserialize().
Ведь в вашем примере данные только заносятся в массив и никак не используются…
Честно говоря, я сам четко не представляю что нужно делать в этом тесте. Основная проблема, по-моему в том, что использования данных зависит от конкретных задач, а значит результаты теста будут справедливы только для них.
Дело в том, что этот тест был восстановлен по памяти. И предполагаемые объемы данных там были не 1 мегабайт (изначальый), а около 10 мегабайт. И раскидывались примерно по такой же структуре.
К сожалению, реальный код привести не могу — он до ужаса пропиеритарный и писался на заказ, два года назад. И проблема, с которой я столкнулся, как раз и было — глубокое зависание машины из-за свапа 2х гигабайт используемой памяти.
Лучщим тестом для этого решения было то, что система начала работать (и работает по сей день).
Да, кстати, в этом же проекте я приучился в случае таких объемов использовать unset везде где только можно. Тоже сэкономило кусочек памяти (соответственно — меньший своп).
Учитывая, что в данном случае “тормознутость скрипта” не сильно влияет на остальные компонетны системы (да, пользователь ждет немного дольше), а вот в случае свопа — эта штука, как часть архитектуры, тормозит _все_. Независимо от приоритетов.