На этот пост меня вдохновило исследование потребления памяти для моего текущего большого проекта на ZendFramework. Как обычно, по результатам исследования, я был шокирован нашей программистской самонадеянностью, которая нередко присутствует, когда мы пишем что-либо большое на PHP. Да и, наверное, не только на PHP.
Но обо всём по порядку.
Данная статья является логическим продолжением следующих статей:
Такой шаблон подходит для измерения новой выделяемой памяти, то есть памяти под переменные. А вот измерить, сколько едят определения, то есть описания функций и классов, таким подходом нельзя, так как они заносятся в память до начала выполнения скрипта. Для того чтобы измерить определения, воспользуемся следующим шаблоном:
Где $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-акселераторы при запуске из консоли не работают. Вот результаты:
Название теста
Описание
Ubuntu x86,
PHP 5.3.10,
ZF 1.11.11
Ubuntu x86-64,
PHP 5.3.10,
ZF 1.11.11
Gentoo x86-64,
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
Результаты приведены только тестов, где акселератор оказывает влияние:
Описание класса 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();
чтобы проверить, какое максимальное количество памяти пожирал скрипт во время генерации страницы. Результаты:
Дополнительно проверялась сборка под Gentoo, он оказался на 25% эффективнее во всех тестах.
Выводы
Если память дорогой ресурс (например VPS) и не особо нужны 64-битные числа, то есть смысл использовать 32-битную версию ОС. Выигрыш будет ~ в 1.8 раза.
В ОС, в которых происходит заточка пакетов под текущую архитектуру можно дополнительно сэкономить 25% памяти.
Ничто так не потребляет память в PHP, как тяжёлый фреймворк. Использование акселератора не спасает от поедания памяти тяжёлыми фреймворками. Возможно имеет смысл ознакомиться со следующим сравнением PHP фреймворков, чтобы выбрать для себя баланс популярности/производительности.
Ситуацию, которая изображена на картинке для привлечения внимания, можно получить, если размер APC кеша окажется исчерпан. Этого добиться не сложно, если у вас много сайтов на одной машине, а вы установили APC, не проверяя хватит ли вам памяти. При этом статистика (apc.php) вам будет сообщать, что у вас есть ещё около 40% памяти, но ей особо не следует верить, ибо у APC плохой менеджер памяти и он просто не умеет использовать её эффективно. Лучше всегда обращайте внимание на hits и miss значения.