13 December 2012

Сколько памяти потребляют объекты в PHP и стоит ли использовать 64-битную версию?

PHP


На этот пост меня вдохновило исследование потребления памяти для моего текущего большого проекта на ZendFramework. Как обычно, по результатам исследования, я был шокирован нашей программистской самонадеянностью, которая нередко присутствует, когда мы пишем что-либо большое на 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-акселераторы при запуске из консоли не работают. Вот результаты:
Название теста Описание 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

Результаты приведены только тестов, где акселератор оказывает влияние:
Название теста Описание Ubuntu x86,
PHP 5.3.10,
ZF 1.11.11,
Empty cache
Ubuntu x86,
PHP 5.3.10,
ZF 1.11.11,
Refresh
Ubuntu x86-64,
PHP 5.3.10,
ZF 1.11.11,
Empty cache
Ubuntu x86-64,
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();
чтобы проверить, какое максимальное количество памяти пожирал скрипт во время генерации страницы. Результаты:
Тип страницы Ubuntu x86,
PHP 5.3.10,
ZF 1.11.11,
Empty cache
Ubuntu x86,
PHP 5.3.10,
ZF 1.11.11,
Refresh
Ubuntu x86-64,
PHP 5.3.10,
ZF 1.11.11,
Empty cache
Ubuntu x86-64,
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. Официальная документация это также подтверждает.
Tags:phpоптимизацияпотребление памяти
Hubs: PHP
+47
60.7k 252
Comments 49