На этот пост меня вдохновило исследование потребления памяти для моего текущего большого проекта на ZendFramework. Как обычно, по результатам исследования, я был шокирован нашей программистской самонадеянностью, которая нередко присутствует, когда мы пишем что-либо большое на PHP. Да и, наверное, не только на PHP.
Но обо всём по порядку.
Данная статья является логическим продолжением следующих статей:
- Работа с памятью (и всё же она есть)
- Насколько большие массивы (и значения) в PHP? (Подсказка: ОЧЕНЬ БОЛЬШИЕ)
Как будем измерять
Для начала определимся, как мы будем измерять «вес». Вот шаблон:
$startMemory = 0;
$startMemory = memory_get_usage();
// Измеряемое
echo (memory_get_usage() - $startMemory) . ' bytes' . PHP_EOL;
Такой шаблон подходит для измерения новой выделяемой памяти, то есть памяти под переменные. А вот измерить, сколько едят определения, то есть описания функций и классов, таким подходом нельзя, так как они заносятся в память до начала выполнения скрипта. Для того чтобы измерить определения, воспользуемся следующим шаблоном:
$startMemory = 0;
$startMemory = memory_get_usage();
// Измеряемое
include $testfile;
echo (memory_get_usage() - $startMemory - $include_overhead) . ' bytes' . PHP_EOL;
Где $include_overhead — сколько отжирает оператор include под свои внутренние нужды. В этой статье мы не будем изучать, как мы можем измерить $include_overhead. Замечу только, что размер пожираемой памяти зависит от 3 вещей:
- Длины абсолютного пути до файла
- Каким по счёту этот файл был включён (каждые 8, 16, 32, 64 и т.д. идёт дополнительное выделение под внутренние структуры)
- Заполненностью внутренних структур самого PHP, которые периодически довыделяют себе памяти на будущее.
Если кому-нибудь интересно разобраться в этом глубже, то можете изучить файл run.include-test.php, он очень хорошо иллюстрирует неравномерность пожирания памяти при include. Также отмечу, что во всех тестах ниже мы измеряем $include_overhead примерно, ибо нам нужны не точные значения а тенденция и различия между 32-битной и 64-битной версией.
Сколько весят «объекты»
Итак был написан TestSuite для автоматического запуска большого количества тестов. Все тесты запускались в VirtualBox для Ubuntu 12.04.1 LTS i386 и Ubuntu 12.04.1 LTS amd64. Версия PHP — 5.3.10, ZendFramework — 1.11.11. Команда для запуска в консоли:
php run.testsuite-without-accelerator.php
Дополнительно я сделал тест на своей машине с Gentoo amd64 для контроля. PHP-акселераторы при запуске из консоли не работают. Вот результаты:PHP 5.3.10, ZF 1.11.11 |
PHP 5.3.10, ZF 1.11.11 |
PHP 5.3.15, ZF 1.11.4 |
||
---|---|---|---|---|
a.mention_variable | Упоминание переменной | 44 | 80 | 48 |
a.new_null_variable | Создание новой переменной со значением null | 108 | 208 | 144 |
a.unset_null_variable | Удаление переменной | -108 | -208 | -144 |
stdClass.new | Создание объекта | 120 | 232 | 168 |
stdClass.tovar1 | Создание объекта и ссылки $a на него | 264 | 512 | 352 |
stdClass.tovar2_unset_and_thesame | Удаление ссылки $a и пересоздание ссылки $a | 0 | 0 | 0 |
stdClass.tovar3_unset_and_another | Удаление ссылки $a и создание ссылки $b | 0 | 0 | 0 |
stdClass.tovar4_another | Создание объекта и ссылки $c на него | 264 | 512 | 352 |
stdClass.tovar5_addlink | Создание ссылки $a на тот же объект что и $b | 64 | 128 | 96 |
stdClass.z.free_memory | Удаление ссылок $a, $b и $c | -592 | -1152 | -800 |
myclass.a.empty | Описание класса A | 700 | 1344 | 1128 |
myclass.aa.interface | Описание интерфейса A | 700 | 1344 | 1128 |
myclass.ab.final | Описание финального класса AB | 700 | 1344 | 1128 |
myclass.ac.abstract | Описание абстрактного класса AC | 700 | 1344 | 1128 |
myclass.b.extended.empty | Описание класса B, расширяющего A | 700 | 1344 | 1128 |
myclass.c.empty.namespace | Описание пустого неймспейса C | 0 | 0 | 0 |
myclass.d.construct | Описание класса D с конструктором | 1104 | 2288 | 1920 |
myclass.dd.method | Описание класса DD с методом | 1088 | 2280 | 1912 |
myclass.ddd.private.var | Описание класса DDD с приватной переменной | 960 | 1840 | 1472 |
myclass.dddd.public.var | Описание класса DDDD с публичной переменной | 960 | 1840 | 1472 |
myclass.ddddd.static.var | Описание класса DDDDD со статической переменной | 960 | 1840 | 1472 |
myclass.e.extended.destruct | Описание класса E с деструктором, расширяющим класс D | 1344 | 2704 | 2272 |
myclass.e.instance.ab | Создание объекта AB и ссылки $e на него | 264 | 512 | 352 |
myclass.e.instance.ddddd | Создание объекта DDDDD и ссылки $e на него | 0 | 0 | 0 |
myclass.e.instance.e | Создание объекта E и ссылки $e на него | 0 | 0 | 0 |
myclass.f.instance.ddddd | Создание объекта DDDDD и ссылки $f на него | 264 | 512 | 352 |
myclass.z.free_memory | Удаление ссылок $e, $f | -484 | -944 | -656 |
zend.a.init.autoload | Инициализация autoload для ZendFramework | 127 444 | 276 288 | 249 232 |
zend.a.init.model | Инициализация адаптера по умолчанию для базы | 1 018 388 | 2 081 600 | 1 871 256 |
zend.extended.controller1 | Определение контроллера от Zend_Controller_Action. Попутно происходит подгрузка стандартных зендовских классов | 378 296 | 809 384 | 712 816 |
zend.extended.controller2 | Определение контроллера. Класы Zend уже подгружены, смотрим, сколько весит наш класс | 11 328 | 19 608 | 16 008 |
zend.extended.model1 | Определение модели от Zend_Db_Table. Попутно происходит подгрузка стандартных зендовских классов. | 27 936 | 48 544 | 40 224 |
zend.extended.model2 | Определение модели. Класы Zend уже подгружены, смотрим, сколько весит наш класс | 27 936 | 48 536 | 40 208 |
zend.use.model1.e.instance1 | Создание объекта Model1 и ссылки $e на него | 2492 | 4648 | 3432 |
zend.use.model1.f.instance2 | Создание объекта Model1 и ссылки $f на него | 1764 | 3256 | 2488 |
zend.use.model1.g.instance3 | Создание объекта Model1 и ссылки $g на него | 1764 | 3256 | 2488 |
zend.use.model2.e.instance1 | Создание объекта Model2 и ссылки $e на него | 740 | 1400 | 944 |
zend.use.model2.f.instance2 | Создание объекта Model2 и ссылки $f на него | 0 | 0 | 0 |
Можно заметить, что сборка Gentoo потребляет на 10-20% меньше памяти, а в редких случаях экономия доходит до 50%. Видимо, размер внутренних структур зависит от оптимизаций для процессора. Для экперимента я пересобирал php с разными вариантами CFLAGS, но он от этого не стал потреблять больше. Видимо разница проявляется не из-за пересборки самого PHP, а из пересборки стандартных Сишных библиотек.
Как было отмечено выше, точно измерить $include_overhead сложно, поэтому если вы запустите данные тесты, то у вас могут получится так, что потребление памяти будет прыгать на 4, 8, 12, 16 байт, даже в тестах, которые должны потреблять одинаково. Не стоит акцентировать на этом внимания. Я запускал тесты в разном порядке и более-менее установил истинное потребление памяти.
Поговорим о тестах, связанных с ZendFramework. Загрузка определений классов Zend`а в память отжирает существенные ресурсы, тогда как ссылки на объекты уже потребляют не так много. Controller2 нужен, чтобы проверить, сколько будет отжирать аналогичный контроллер, если все промежуточные классы уже в памяти. Model2 создана для этих же целей.
В потенциале использование PHP акселератора сэкономит нам память на всех определениях, ибо они уже будут храниться в памяти. Давайте проверим это утверждение.
Тестирование акселераторов
Для тестирования был взят APC, и тесты запускались через web с помощью скрипта:
php run.testsuite-with-accelerator.php
Результаты приведены только тестов, где акселератор оказывает влияние:
PHP 5.3.10, ZF 1.11.11, Empty cache |
PHP 5.3.10, ZF 1.11.11, Refresh |
PHP 5.3.10, ZF 1.11.11, Empty cache |
PHP 5.3.10, ZF 1.11.11, Refresh |
||
---|---|---|---|---|---|
myclass.a.empty | Описание класса A | 840 | 672 | 1480 | 1256 |
myclass.aa.interface | Описание интерфейса A | 856 | 676 | 1512 | 1264 |
myclass.ab.final | Описание финального класса AB | 844 | 672 | 1488 | 1256 |
myclass.ac.abstract | Описание абстрактного класса AC | 852 | 680 | 1504 | 1264 |
myclass.b.extended.empty | Описание класса B, расширяющего A | 912 | 700 | 1512 | 1264 |
myclass.c.empty.namespace | Описание пустого неймспейса C | 176 | -16 | 184 | -72 |
myclass.d.construct | Описание класса D с конструктором | 1256 | 960 | 2448 | 1736 |
myclass.dd.method | Описание класса DD с методом | 1268 | 968 | 2432 | 1728 |
myclass.ddd.private.var | Описание класса DDD с приватной переменной | 1140 | 964 | 2000 | 1760 |
myclass.dddd.public.var | Описание класса DDDD с публичной переменной | 1132 | 952 | 2000 | 1760 |
myclass.ddddd.static.var | Описание класса DDDDD со статической переменной | 1124 | 952 | 2000 | 1760 |
myclass.e.extended.destruct | Описание класса E с деструктором, расширяющим класс D | 1528 | 1228 | 2888 | 2160 |
myclass.z.free_memory | Удаление ссылок $e, $f | -332 | -548 | -784 | -1024 |
zend.a.init.autoload | Инициализация autoload для ZendFramework | 127 596 | 16 196 | 276 440 | 28 992 |
zend.a.init.model | Инициализация адаптера по умолчанию для базы | 1 018 564 | 251 840 | 2 081 696 | 479 280 |
zend.extended.controller1 | Определение контроллера от Zend_Controller_Action. Попутно происходит подгрузка стандартных зендовских классов | 378 464 | 66 804 | 809 608 | 120 864 |
zend.extended.controller2 | Определение контроллера. Класы Zend уже подгружены, смотрим сколько весит наш класс | 11 476 | 11 140 | 19 792 | 19 056 |
zend.extended.model1 | Определение модели от Zend_Db_Table. Попутно происходит подгрузка стандартных зендовских классов. | 28 080 | 25 676 | 48 704 | 42 944 |
zend.extended.model2 | Определение модели. Класы Zend уже подгружены, смотрим, сколько весит наш класс | 28 080 | 25 704 | 48 672 | 42 960 |
Я также производил некоторые тесты с xcache и заметил 2 отличия от APC. Во-первых: xcache проигрывает (почти всегда) на 10-15% по экономии памяти. А во-вторых: xcache сразу отдаёт файлы из кеша, тогда как APC — только после повторного обращения. Хоть и бесполезное, но преимущество.
Сразу отмечу, в результатах разброс гораздо больше, чем при тестировании без акселератора, поскольку файлы не переименовывались и $include_overhead рассчитывался с большой ошибкой.
Как мы видим, акселератор хоть и экономит нам память для определений, но не полностью, поскольку PHP, видимо, переносит какие-то куски из кеша в текущую сессию.
Теперь перейдем от абстрактных тестов к вполне реальным.
Тестирование небольшого приложения на ZendFramework
Для тестирования было взято тестовое задание одного из наших программистов (Simple-blog): сервис коллективного блога с функциями: регистрации, авторизации, чтения списка постов, открытия поста и его комментирования. В конце index.php было написано:
echo memory_get_peak_usage();
чтобы проверить, какое максимальное количество памяти пожирал скрипт во время генерации страницы. Результаты:PHP 5.3.10, ZF 1.11.11, Empty cache |
PHP 5.3.10, ZF 1.11.11, Refresh |
PHP 5.3.10, ZF 1.11.11, Empty cache |
PHP 5.3.10, ZF 1.11.11, Refresh |
|
---|---|---|---|---|
Список постов | 5 328 648 | 1 792 968 | 10 938 160 | 3 306 720 |
Пост и его коментарии | 5 372 356 | 1 831 452 | 11 015 320 | 3 373 528 |
Логин форма | 6 781 656 | 2 277 164 | 13 982 104 | 4 187 600 |
Форма регистрации | 6 796 496 | 2 291 568 | 14 009 384 | 4 211 432 |
Дополнительно проверялась сборка под Gentoo, он оказался на 25% эффективнее во всех тестах.
Выводы
- Если память дорогой ресурс (например VPS) и не особо нужны 64-битные числа, то есть смысл использовать 32-битную версию ОС. Выигрыш будет ~ в 1.8 раза.
- В ОС, в которых происходит заточка пакетов под текущую архитектуру можно дополнительно сэкономить 25% памяти.
- Ничто так не потребляет память в PHP, как тяжёлый фреймворк. Использование акселератора не спасает от поедания памяти тяжёлыми фреймворками. Возможно имеет смысл ознакомиться со следующим сравнением PHP фреймворков, чтобы выбрать для себя баланс популярности/производительности.
- Ситуацию, которая изображена на картинке для привлечения внимания, можно получить, если размер APC кеша окажется исчерпан. Этого добиться не сложно, если у вас много сайтов на одной машине, а вы установили APC, не проверяя хватит ли вам памяти. При этом статистика (apc.php) вам будет сообщать, что у вас есть ещё около 40% памяти, но ей особо не следует верить, ибо у APC плохой менеджер памяти и он просто не умеет использовать её эффективно. Лучше всегда обращайте внимание на hits и miss значения.
Кодяра
UPD
AntonShevchuk добавил результаты для тестов на PHP 5.4. PHP 5.4 выглядит гораздо экономичнее по сравнению с 5.3. Официальная документация это также подтверждает.