Нестандартная оптимизация проектов на PHP

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

image

Традиционные методы, думаю, всем известны:
  • Оптимизация SQL-запросов;
  • Поиск и исправление узких мест;
  • Переход на Memcache для часто используемых данных;
  • Установка APC, XCache и подобных;
  • Клиентская оптимизация: CSS спрайты и т.п.


В нашем же проекте всё это было сделано, но при этом проблема скорости обработки страниц сохранялась. Средняя скорость обработки страницы была в районе 500мс. В один прекрасный момент пришла идея проанализировать, какие ресурсы есть, и на что они могут тратиться.

После анализа были выявлены следующие основные ресурсы, которые нужно мониторить:

  1. Процессорное время;
  2. Оперативная память;
  3. Время ожидания других ресурсов (MySQL, memcache);
  4. Время ожидания диска.

В нашем проекте мы почти не используем запись на диск, поэтому сразу исключили 4-й пункт.

Дальше начался поиск узких мест.

Этап 1


Первое, что было испробовано — профайлинг MySQL запросов, поиск медленных запросов. Подход не принёс особого успеха: несколько запросов было оптимизировано, но среднее время обработки страниц не сильно изменилось.

Этап 2


Дальше была попытка профайлинга кода с помощью XHProf. Это дало ускорение в узких местах и смогло снизить нагрузку примерно на 10-15%. Но основной проблемы это тоже не решило. (Если будет интересно, могу отдельно написать статью о том, как оптимизировать с помощью XHProf. Вы только дайте знать в комментариях.)

Этап 3


В третьем этапе пришла мысль посмотреть, сколько памяти уходит на обработку запроса. Оказалось, что в этом и есть проблема – простой запрос может требовать загрузить до 20мб кода в ОЗУ. Причём причина этого была непонятна, так как идёт простая загрузка страницы — без запросов в базу данных, или загрузки больших файлов.

Было принято решение написать анализатор и выяснить, сколько памяти требует каждый пхп файл при его включении.

Анализатор очень простой: в проекте уже был автозагрузчик файлов, который на основе имени класса сам подгружал нужный файл (autoload ). В нём просто было добавлено 2 строки: сколько памяти было до загрузки файла, сколько стало после.

Пример кода:

Profiler::startLoadFile($fileName);
Include $fileName;
Profiler::endLoadFile($fileName);

Первая строка сохраняет, сколько памяти было в начале, последняя строка – вычитает разницу и добавляет в список загрузок. Так же профайлер сохраняет очередь загрузки файла (backtrack).

В конце выполнения всего кода мы добавили вывод панели с собранной информацией.

echo Profiler:showPanel();

Анализ показал очень интересные причины того, почему может использоваться излишняя память. После того, как мы проанализировали все страницы и убрали загрузку лишних файлов страница стала требовать менее 8мб памяти. Обновление страницы стало быстрее, нагрузка на сервера уменьшилась и на тех же машинах появилась возможность обрабатывать больше клиентов.

Дальше идёт список вещей, которые мы изменили. Здесь стоит отметить, что сам функционал не изменился. Изменилась только структура кода.

Все примеры сделаны специально упрощёнными, так как предоставить оригинальный код проекта возможности нет.

Пример 1: Загрузка, но не использование больших родительских классов


Пример:

class SysPage extends Page{
    static function helloWorld(){
        echo "Hello World";
    }
}

Файл сам по себе очень маленький, но при этом, он тянет за собой по крайней мере один другой файл, который может быть достаточно большой.

Решений несколько:

  1. Убрать наследование где не нужно;
  2. Сделать чтобы все родительские классы были минимальные по размерам.


Пример 2: Использование длинных классов, хотя нужна только малая часть класса


Очень похоже на предыдущий пункт. Пример:

class Page{
    public static function isActive(){}
    // тут длинный код на 1000-2000 строк
}

Естественно, что PHP не знает, что вам нужна только 1 функция. Как только идёт обращение к данному классу, идёт подгрузка всего файла.

Решение:
Если есть подобные классы, то стоит выделить функционал который используется везде в отдельный класс. В текущем классе можно ссылаться на новый класс для совместимости.

Пример 3: Подгрузка большого класса только для констант


Пример:

$CONFIG['PAGES'] = array(
    'news' => Page::PAGE_TYPE1,
    'about' => Page::PAGE_TYPE2,
);

Или:

if(get('page')==Page::PAGE_TYPE1){}

Т.е. пример похож на предыдущий, только теперь константа. Решение такое же как и в прошлом случае.

Пример 4: Авто создание в конструкторе классов, которые возможно и не будут использованы


Пример:

class Action{
    public $handler;
    public function __construct(){
        $this->handler = new Handler();
    }

    public function handle1(){}
    public function handle2(){}
    public function handle3(){}
    public function handle4(){}
    public function handle5(){}
    public function handle6(){}
    public function handle7(){}
    public function handle8(){}
    public function handle9(){}
    public function handle10(){
        $info = $this->handler->getInfo();
    }
}

Если Handler используется часто, то проблемы нет. Совсем другой вопрос, если он используется только в 1 из функций из 20.

Решение:
Убираем из конструктора, переходим на ленивую загрузку через магический метод __get или же например так:

public function handle10(){
    $info = $this->handler()->getInfo();
}
public function handler(){
    if($this->handler===null)
        $this->handler = new Handler();
    return $this->handler;
}

Пример 5: Подгрузка ненужных языковых файлов / конфигов


Пример: У вас есть большой файл с настройками всех страниц всех пунктов меню. В файле может быть много массивов.

Если часть данных используется редко, а часть часто – то данный файл является кандидатом на разделение.

Т.е. стоит подгружать данные настройки только тогда, когда они нужны.

Пример использований памяти у нас:

Файл в 16 кб, просто массив данных – требует от 100 кб и выше.

Пример 6: Использование serialize/unserialize


Часть настроек, которые часто меняются, мы хранили в файле в формате serialize. Мы обнаружили, что это загружает как процессор, так и оперативную память, причём у нас получилось, что на PHP версии 5.3.х у функции unserialize очень сильная утечка памяти.

После оптимизации мы максимально избавились от этих функций где возможно. Данные в файлах мы решили хранить в виде массива, сохранённого через var_export и загружать обратно через с помощью include/require. Таким образом мы смогли задействовать APC режим.

К сожалению, для данных которые хранятся в memcache, данный подход не работает.

Итоги


Все данные примеры легко находятся и очень легко правятся не сильно меняя структуру проекта. Мы взяли правило: «любые файлы, которые требуют более 100кб, надо проверять на предмет можно ли оптимизировать их загрузку. Так же мы смотрели на ситуации, когда загружалось несколько файлов из одной ветки. В этой ситуации мы смотрели, можно ли вообще не загружать всю ветку файлов. Ключевая мысль была такой: «всё, что мы загружаем, должно иметь смысл. Если можно не загружать каким-либо способом – лучше не загружать.

Заключение


После того, как мы убрали всё, что выше, один запрос к серверу стал требовать примерно в 2 раза меньше оперативной памяти. Нам это дало уменьшение нагрузки процессора примерно в 2 раза без перенастройки серверов и уменьшение средней скорости загрузки одной страницы в несколько раз.

Дальнейшие планы


Если будет интересно, есть в планах идея описать способы профилирования кода и поиска узких мест с помощью XHprof.
Share post

Comments 103

    +18
    > Если будет интересно, есть в планах идея описать способы профилирования кода и поиска узких мест с помощью XHprof.
    Интересно!
      +2
      Хорошо. Пока думаю на тему — какой проект для этого использовать. Тут нужен реальный проект для примера, но при этом — не могу использовать свой рабочий проект.
      В добавок — проект не должен быть слишком сложный, иначе примеры будут слишком сложнопонимаемые :)
        +3
        Возьмите любую не очень популярную CMS, забитую тестовыми данными, я думаю сгодится для тестов.
          0
          Wordpress :-)
          И к реальности ближе и думаю все сталкивались с проблемами этой CMS на виртуальных хостингах
            +1
            О боже, только не вордпресс. Это же кусок, извините, говнокода с недоООП реализацией.
            –1
            Битрикс! Идеальный кандидат. Полагаю, сайты на опенсорсных CMS редко когда надо столь глубоко оптимизировать. Обычно, это визитки/каталоги.
        +11
        Можно уточнить, что в данной оптимизации является нестандартным?
        Профайлинг mysql запросов — вполне себе best practics,
        рефакторинг кода, особенно после адекватного анализа — тоже вполне разумный и популярный шаг,

        То есть я бы сказал, что вы все сделали верно, даже профессионально, но где же изюминка?
          +1
          Именно поэтому я не хотел обращать внимание на профайлинг mysql запросов. Подобный профайлинг описан во большом количестве мест, и писать о нём ещё раз — не интересно :)

          Самые главные мысли которые я хотел донести:
          1. Показать необычный кейс, о котором многие разработчики даже не задумываются, и возможно помочь кому либо оптимизировать свой код.
          1. Нужно внимательно анализировать даже на те вещи, которые маловероятны.
          2. Соблюдение всех ООП практик по разделению кода на классы — может повлиять негативно в плане использования памяти. Есть большая вероятность что процессору придётся обрабатывать ненужные вещи. (тут конечно можно поспорить на тему «что такое лучшие ООП практики»)
            0
            Если взять ВСЕ ООП практики, то они вполне могут и противоречить друг другу.

            Именно поэтому для крупных проектов нужен не senior programmer а program architect, который спроектирует хотя бы ядро системы и общую политику для проекта.
            НО, это опять таки best practice ;)
              +8
              Мне лично больше интересует почему вообще возникла такая проблема, что 20мб памяти на запрос это много и тормозит. Меня крайне интригует этот момент. Тут больше не потребление памяти наверное играло роль, а то что все грузилось в рамках одного запроса, даже то что не должно. В этом плане какой-нибудь IoC контейнер решил бы массу проблем. Ну и так же интересно на какой версии PHP у вас все это крутилось, установлен ли opcache и с какими настройками. Сколько вообще ОЗУ.
                +3
                Ну и да, я думаю если бы вы обновились хотя бы до php 5.4 уже был бы ощутим прирост производительности.
                  +2
                  я щас может кого шокирую =) у нас рельсовое приложение кушает по 800мб на воркера, и всё достаточно шустро работает.
                  так что 20мб это сказочная цифра
                    0
                    Рельсы — всё же не PHP. В питоне тоже нет подобной проблемы, насколько я знаю.

                    В PHP же проблема что сервер умеет кешировать код, но при этом, ему приходится загрузить этот кешированный файл в рабочую область текущего запроса. И на это уходит полезный ресурс — память и соответственно время
                      +3
                      Воу воу, opcache хранит все в shared memory. Вообще никакого оверхэда за счет этого, один и тот же кэш на все воркеры. Более того, начиная с php5.4 появилась куча внутренних оптимизаций, сильно сокращающих оверхэд по использованию памяти. opcache же расширяет действия некоторых оптимизаций храня данные не для воркера а для всего пула воркеров.

                      Если обновить php с 5.3 до 5.5 то это уже может дать прирост в 30% если ситуация именно такая, как описана в статье.

                      У PHP проблема в том что он умирает. Есть примеры где приложение запущенное из под ReactPHP в качестве сервера работало ощутимо быстрее, потому как часть сервисов можно вообще не выгружать между запросами.
                        +3
                        У PHP проблема в том что он умирает.

                        Почему, я бы сказал воскресает.
                        0
                        Представьте что у вас есть огромный массив данных в файле.*

                        Этот массив был обработан, преобразован в opcache и добавлен в shared memory. Всё выглядит ок.

                        Теперь, как только вы захотите загрузить данный массив в текущую рабочую область конкретного запущенного скрипта — он скопируется и будет использовать память из текущего потока.

                        Другими словами: если вы хотите использовать массив на 10мб, то каждый скрипт должен использовать как минимум 10 мб памяти. И да, получится что каждый скрипт копирует одни и теже данные к себе в рабочую область из shared области.
                        Но в shared области — данные хранятся «в общем виде», а в «текущей рабочей области» — хранятся только для данного скрипта, причём с возможность быстрого чтения по индексу, и тп.

                        Абсолютно таже проблема, если мы берём не «массив», а «класс». Нам нужно его из shared памяти перетащить в «рабочую память».

                        PS:
                        * Сейчас не идёт разговор о том, как оптимизировать скрипт так, чтобы он не читал этот файл вообще, или читал его куски только. Это тоже можно и нужно делать, но это совсем другой вопрос.
                          +4
                          Возможно я чего-то не знаю, но мне казалось что разделяемая память на то и нужна что бы не нужно было ничего копировать в локальное адресное пространство процесса. Просто несколько процессов делят между собой одно адресное пространство, через mmap или другие штуки специфичные для своей платформы.

                          Ну то есть возможно копирование данных там и идет (что бы не допускать сильно долгих блокировок и конфликтов) но это же не обязательно.
                            0
                            Вы можете доказать, что опкоды копируются из shared memory в область скрипта?
                              +1
                              возьмём пример:
                              мы делаем include файла в котором только описание класса (или массив).
                              У нас стоит opcache/apc, и мы не первый раз загружаем данный файл, один и тот же поток апача, по мониторингу apc/opcache — происходит cache hit.

                              Просто include данного файла, без создания экземпляра данного класса — увеличивает потребление памяти текущим пхп скриптом. Вопрос — на что это тратиться?

                              Т.е. пример:
                              $memory = memory_get_usage();
                              include «class.php»;
                              echo memory_get_usage()-$memory;

                              Почему увеличилось использование памяти? Ведь данные должны быть общими для всех запросов, и мы могли бы их использовать напрямую из shared memory.

                              Могу перефразировать, возможно у меня неточность формулировки:
                              копируются не опкоды, а результат их выполнения в текущую область скрипта. Т.е когда в файле только описание класса, то opcache его преобразует в опкоды 1 раз. После этого, каждый раз при использовании данного файла, пхп выполняет опкоды, и результат выполнения опкодов — добавляется в текущую рабочую область скрипта каждый раз.

                              Такая формулировка понятнее? :)
                                +1
                                gnomeby
                                Я бы даже сказал что вы сами пришли к подобному умозаключению в своей же статье :)

                                Ваша же фраза оттуда:
                                Как мы видим, акселератор хоть и экономит нам память для определений, но не полностью, поскольку PHP, видимо, переносит какие-то куски из кеша в текущую сессию.
                                  0
                                  Молодец, поймал.
                                  Даааа, давно это было.
                                  0
                                  Да, такая формулировка ставит всё на свои места.
                                    +1
                                    Почему увеличилось использование памяти? Ведь данные должны быть общими для всех запросов, и мы могли бы их использовать напрямую из shared memory.


                                    Ну во первых какие-то накладные расходы все равно выходят, но это капля в море и вообще не должна влиять на производительнось. Простой SQL запрос по сравнению со всем этим будет казаться вечностью.

                                    Во вторых, с точки зрения этого самого процесса, все адресное пространство его и только его. Процессу должно быть пофигу что кусок адресного пространства разделен между несколькими процессами. Посему если у нас процесс потребляет 10 метров своей памяти и между всеми процессами есть общая память в 2 метра то суммарное потребление будет показываться как 12 для каждого из них. Я предполагаю что mem_get_usage не сильно различает какая именно память была скушана в рамках этого запроса.

                                    Возможно я не прав, я не сильно шарю в IPC и в том как это реализовано в PHP. Но все же, если судить по вашей логике, в оптимизациях типа string interning не особо много смысла.
                            –5
                            Рельсовое приложение грузится один раз при запуске, а пхпшное при каждом запросе.
                            +1
                            > Тут больше не потребление памяти наверное играло роль, а то что все грузилось в рамках одного запроса, даже то что не должно.
                            Да, проблему можно сократить и до этого.

                            Крутилось всё на пхп 5.3, с APC. Обновили до пхп 5.5 с opcache — количество длинных запросов сократилось ещё в 2 раза. При этом количество запросов обрабатываемых на том же железе — увеличилось. (т.е. на том же железе — стало всё быстрее, и при этом нужно меньше железа для обработки тех же данных).

                            При этом, мы конечно сравнивали ситуации на машине с PHP 5.5+ opcache, но с неоптимизированным кодом и с оптимизированным кодом — разница заметна на глаз была.
                              0
                              т.е. приведенные графики — это ситуация 5.5 с opcache?
                                0
                                Нет, в графиках — изменили только то что описано в статье. Т.е. в них — php 5.3 с APC.
                                График с php 5.5 и опкеш — ещё уполовинили красную часть, и увеличил зелёную часть.
                            0
                            Кейс, кстати, часто встречается.

                            По моим наблюдениям, разработчики, как правило, понимают ООП как наследование и в лучшем случае добавить интерфейсов. Причём, если нет наследования (IoC, composition) то многих это ставит в ступор.

                            По поводу практик: вот шикарная книга (Patterns of Enterprise Application Architecture) — можно выбрать те паттерны, которые вам подходят больше всего.
                              0
                              на тему необычности — меня в свое время гораздо больше порадовало, что можно добиться существенного ускорения пхп за счет минимизации кода (как в джаваскрипте — выкидываем проблеы лишние, сокращаем локальные переменные и т.п.) — тупо парсеру нужно будет читать меньше данных
                                +1
                                тупо парсеру нужно будет читать меньше данных

                                PHP 5.4 + opcache + отключить инвалидацию кэша для продакшена и никакой разницы от аглификации (парсить код не нужно, а за счет string interning частое использование длинных названий функций/методов/классов/переменных не так сильно будет влиять на память.

                                Вообще не пойму что тут удивительного… меньше символов распарсить — быстрее разбор. Так у всех интерпритируемых языков. У PHP просто еще такая особенность что без опкод кешеров парситься все будет на каждый запрос.
                                  0
                                  да, согласен, этот мой печальный опыт был во времена молодого пхп 4.3, забавно, что за прошедшие 10 лет принципиальных улучшений не произошло — топикстартер воюет все с теми же граблями.

                                  удивительно было какой сильный эффект это давало в то время, казалось бы такой шаг минификации пхп мог бы и сам сделать в момент чтения к примеру.
                                    +1
                                    казалось бы такой шаг минификации пхп мог бы и сам сделать в момент чтения к примеру.

                                    Что бы минифицировать названия переменных их нужно распарсить. Если вы уже распарсили имена переменных смысла минификация уже не имеет.

                                    за прошедшие 10 лет принципиальных улучшений не произошло — топикстартер воюет все с теми же граблями.

                                    Вы о каких таких граблях? Не оптимизированная/кривая архитектура? Причем тут PHP?
                            +6
                            php-di.org/ и подобные решили бы кучу ваших проблем.

                            Ну а все остальное — архитектурные ошибки.
                              +1
                              Откуда взялась мода советовать контейнеры при оптимизации ?)

                              Контейнеры добавляют удобство разрешения зависимостей, но никак не оптимизирует Ваш код.

                              Как раз наоборот, использование контейнеров добавляет расходы:

                              — маппинг определений и параметров хранится в памяти
                              — расходы на автозагрузку не меняются, более того, грузится еще и библиотека для реализации контейнеров
                              — резолвинг зависимостей тратит время и грузит процессор
                                +3
                                — а где ему еще храниться?
                                — при наличии opcache настроенного, у вас все уже висит в памяти и никаких расходов особо на автозагрузку нет. А при наличии ленивой инициализации контейнер предотвратит от выстрела в ногу в виде «инджектим 100500 классов в один и используем только несколько из них».
                                — хороший контейнер умеет компилировать весь этот граф в быстрый шматок кода, который будет не медленнее (а то и быстрее, в зависимости от кривизны рук) чем просто инициализировать объекты на местах.
                                  0
                                  Смысл моего комментария в том, что использование контейнеров не поможет оптимизироваться смысл Вашего — остается для меня загадкой…

                                  1. Маппинг хранится в памяти и увеличает ее расход, а не уменьшает, значит оптимизации никакой здесь нет.

                                  2. Ну и как тогда может помочь контейнер с автозагрузкой?

                                  3. github.com/mnapoli/PHP-DI/blob/master/src/DI/Container.php#L104 — вот код который достает класс, Вы правда считаете что это быстрее чем «new»?

                                    +1
                                    1. Opcache решает эту проблему, а на память мне плевать (если речь идет о паре десятков килобайт, как в этом случае). Не бывает «и память не жрет и быстро».

                                    2. Opcache решает эту проблему если сделать composer dump-autoload --optimize. Контейнер зависимостей тут не причем.

                                    3. чем просто new — конечно же нет. Но давайте посмотрим правде в глаза, вы всеравно будете делать какой-то свой сервис локатор аля pimple с фабриками и прочим что бы код не превратился в гуано из бойлерплейта. Я думаю когда в PHP-DI наконец запилят компиляцию контейнера, опять же при грамотно настроенном opcache (имеется в виду отключение инвалидации кэша, подстройка сколько памяти оно может есть и т.д.) оверхэд будет минимален и незначителен. Есть так же и другие контейнеры, типа dice или тот же thephpleague/container.
                                      0
                                      Может тогда и советовать использовать opcache, а не контейнеры?)

                                      Я еще раз повторю — контейнеры как способ оптимизировать приложение — бред, их используют абсолютно для других целей.
                                        0
                                        Проблемы описанные в статье — это не проблемы именно алгоритмов или чего-то еще. Это архитектурные прощеты. Там описывается что много чего удалось достичь рефакторингом кода и добавлением отложенной инициализации. Контейнер не сильно поможет решить проблемы архитектуры, это верно, но половину проблем описанных в статье это дело решает. И дело тут не в потреблении памяти, а в том на что эта память расходуется.

                                        Держать же продакшен без opcache/apc вообще нынче смысла нет.
                                        0
                                        прочим что бы код не превратился в гуано из бойлерплейта.

                                        Я не не понимаю ничего в PHP (и не собираюсь) но обычно выбор состоит в том, чтобы это гуано из DI было явным или неявным.

                                        Лично я предпочитаю явное гуано и контролируемый Rumtime Env. вместо того чтобы иметь hidden global state повсюду.

                                        Это особенно болезненно когда хочется иметь хороший и быстрый код и нужно начинать писать тесты. Вот тогда познается весь «сок» магических посредников.
                                          0
                                          Лично я предпочитаю явное гуано и контролируемый Rumtime Env.

                                          Ваше право, хотя вот честно, я не представляю как именно это «не явное» (если можете, приведите пример, возможно я вас не верно понял) мешает писать тесты. Принцип инверсии зависимостей соблюдается, вы как инджектили зависимости в конструктор объектов так и инджектите. А в тестах все зависимости мокаются и на DI вообще как бы пофигу. В случае с интеграционными/функциональными тестами так же нет проблем.
                                            0
                                            Есть просто очень много разных методов DI. В каждой среде по своему.

                                            Я скорее о том чтобы действительно можно без напряга мокать все что пожелаешь.
                                            И чтобы зависимости были явными. Все остальное менее важно.

                                            Ваше право, хотя вот честно, я не представляю как именно это «не явное»

                                            Не явное, это тот вид жести, когда по коду разбросаны вызовы к глобальным переменным/состояниям :)
                                            Или которые не явно затягивают переменные из родительского scope

                                            Если про сервера, то это особенно касается Node/Python/Ruby/Java где приложение постоянно крутится на порте
                                            с каким-то состоянием.

                                            Еще всякие синглтоны этим ой как грешат.

                                            А в тестах все зависимости мокаются

                                            Если код написан так что их можно «мокнуть» (:
                                              +1
                                              Ну я говорил про нормальный IoC контейнер, где от магии только проверка какие реализации каких интерфейсов хочет получить сервис, а дальше в зависимости от конфигурации. То есть никаких проблем с тестированием (если этих проблем не создал разработчик но тут уж IoC контейнер не сильно поможет).
                                                0
                                                Это просто когда нету состояния у запрашиваемого объекта

                                                Вот когда нужно получить объект с внутренним состоянием, тогда начинаются вопросы правильной постройки.
                                                И разработчик без хорошего понимания среды напишет код не очень хорошо.

                                                Среда выполнения каждого языка имеет свои особенности. В С++ люди себе стреляют в ногу. В остальных
                                                хочется выстрелить кому-то в голову (:
                                                  0
                                                  Это просто когда нету состояния у запрашиваемого объекта


                                                  Ну по сути контейнер создает инстанс только если он его еще не создавал (по сути шаблон одиночка), так что с состоянием у запрашиваемого объекта проблем нет.
                                                    0
                                                    За Singleton в приличном обществе можно и нужно получить по лицу (:
                                                      0
                                                      Там где его используют не по назначению. Вы еще скажите что goto это абсолютное зло.
                                                        0
                                                        Хотя бы один кейс хорошего использования Singleton?
                                                          0
                                                          Если вам нужно что бы в пределах приложения (в контексте PHP это обработка одного запроса) был только один инстанс сервиса — то тут стоит применить сингелтон. Собственно это основная идея сего паттерна.

                                                          В контексте DI это будет выглядеть так:
                                                          $instances = [];
                                                          
                                                          if (!isset($instances[$service])) {
                                                              $instances[$service] = invoke($service);
                                                          }
                                                          
                                                          return $instances[$service];
                                                          


                                                          Можно назвать это registry а не singelton, но суть та же.

                                                          Проблема сингелтона не в том что он есть (его же не придумывали чисто как анти-паттерн), а в том как его чаще всего используют.
                                                            0
                                                            Синглетон это антипаттерн сам по себе. Это не явная глобальная переменная. Часто еще и статическая.

                                                            Вот к примеру как вы будете тестировать этот код выше?
                                                              0
                                                              Вы видите где-то у меня статику? А тестить… да как-то так думаю:

                                                              class ContainerSpec extends ObjectBehaviour
                                                              
                                                              function let(Invoker $invoker) {
                                                                  $this->serviceInstance = new ServiceStub();
                                                                  $invoker->invoke('service')->willReturn($serviceInstance);
                                                              
                                                                  $this->beConstructedWith($invoker);
                                                              }
                                                              
                                                              function it_creates_instace_of_service()
                                                              {
                                                                   $this->get('service')->shouldReturn($serviceInstance);
                                                              }
                                                              
                                                              function it_returns_already_created_instance_if_have_one(Invoker $invoker)
                                                              {
                                                                   $invoker->invoke('service')->shouldBeCalledTimes(1);
                                                              
                                                                   $this->get('service')->shouldReturn($this->serviceInstance);
                                                                   $this->get('service')->shouldReturn($this->serviceInstance);
                                                              }
                                                              
                                                              }
                                                              


                                                              Оукей, если мы назавем это Registry вы успокоитесь? Что до статики и «глобальности» — то есть статические методы фабрики для вас тоже дикость? Или статический метод в абстрактном классе который пораждает своих наследников? (нарушая при этом LSP).

                                                              Есть академические каноны и есть прагматизм. Стоит различать когда что применимо.
                                                                0
                                                                Ок, я имел ввиду суть статической переменной. «попробовав раз ем и сейчас».

                                                                Дело не в фабриках и статике. Я не про академию, я про поддерживаемый код одним и более человеком.

                                                                Просто если вы можете выбросить весь свой рантайм то это очень и очень хорошо.
                                                                Насколько я понимаю тут:
                                                                $this->serviceInstance = new ServiceStub(); 

                                                                пересобирается весь «рантайм» а старый выбрасывается на помойку?

                                                                  0
                                                                  Это просто стаб, по которому я проверяю что контейнер возвращает один и тот же объект. Это может быть new stdObject или что угодно что передается по ссылке. В рамках этого теста в принципе это не особо важно, нам важнее узнать что вызывался метод мока и во втором случае вызывался только один раз.

                                                                  Причем тут рантайм не пойму… Я понимаю там… разговоры о разделении на слой приложения и слой фреймворка, слабую связанность и все такое. А рантайм… как его можно выкинуть или пересобрать? Он же рантайм…

                                                                  Ну а что до поддерживаемости — DI как раз и создан для того что бы код был поддерживаемым и поддавался контролю. Разделение ответственности и все такое.
                                                                    0
                                                                    Это просто стаб, по которому я проверяю что контейнер возвращает один и тот же объект

                                                                    Вы в коде можете создать новый контейнер и заменить им старый?
                                                                      0
                                                                      мы сейчас о чем? код который я привел выше это спека для phpspec и с кодом самого контейнера или приложения она никак не связана.

                                                                      Новый контейнер создать можно. Единственное что в рамках одного запроса — не нужно.
                                                                        0
                                                                        Я про программирование, а вы про обработку запросов в PHP. Я его последний раз лет 10 назад читла, не в курсе что там у phpspec

                                                                        Просто если можете заменить контейнер то это не singleton. А если не можете заменить, то это он самый.

                                                                        Singleton (классический) это антипаттерн. Вот собственно и все. Все просто.
                                –3
                                Слишком много ООП!
                                  +1
                                  Использование длинных классов, хотя нужна только малая часть класса
                                  // тут длинный код на 1000-2000 строк


                                  Жесть.
                                  Да даже если ваша приложуха будет отвечать за 50 мс, то такой код держать нельзя.
                                  Мне кажется вам надо срочно вот сюда codeclimate.com
                                  +4
                                  Слушайте, так всё же просто и решается вполне типовыми методами. Используйте DI, следите за количеством зависимостей, не инстанцируйте там, где не надо — и готово. Это не столько оптимизация, сколько банальный рефакторинг.
                                    +4
                                    Ещё можно перейти на версию PHP 5.5+, думаю тоже ощутимо было бы.
                                      0
                                      Вот тоже при упоминании php 5.3 какой-то неприятный спазм произошёл в мозгу. Каждая минорная версия начиная с 5.0 (насколько я помню) существенно быстрее предыдущей. Архитектурные проблемы это не решит, но число узких мест обусловленных платформой существенно снизит.
                                      +3
                                      Поддержку комментаторов выше. Примеры 1-4, по-крайней мере в том виде, в котором вы их описали, — это следствие неправильного использования ООП, попробуйте посмотреть в сторону принципов SOLID.
                                        +2
                                        Сегодняшний PHP не умеет загружать исходный код единожды и шарить его (скомпилированный) между разными обработчиками HTTP-запросов? Если умеет, то какие-то сомнительные пассы здесь приведены по большей части (они в любом случае сомнительные, но если таковы ограничения платформы, то что поделаешь).
                                          +8
                                          начиная с 5.5 php комплектуется opcache-ем, который, если конечно включен, умеет это делать. Если кому интересно, могу расписать что вообще там происходит и как его можно настраивать.
                                            +4
                                            Интересно! Особенно о том, как его можно настраивать и на что при этом смотреть.
                                              0
                                              Ок, начал писать.
                                              0
                                              Поддерживаю, очень интересно!
                                                0
                                                Пишите, конечно!
                                              0
                                              Простите, я правильно понимаю что до этого вы не использовали APC совсем?
                                                0
                                                Нет, не правильно. APC был, сейчас Opcache.
                                                Проблему описал в комментарии выше.

                                                Как работает обработка пхп файла когда вы выполняете подобный код?
                                                include «class.php»;

                                                по шагам, утрированно:
                                                1. читаем с диска
                                                2. преобразуем в op-коды
                                                3. выполняем оп-коды

                                                Пункты 1-2 APC/Opcache делает 1 раз. но пункт 3 приходится делать каждый раз когда вы инклудите этот файл в рабочую область скрипта.
                                                При этом, это происходит жаже если файл — просто описание класса, который никогда не будет выполнен, или из него будет взята только константа. Выполнение этого файла и добавление его в текущую описание занимает как CPU, так и память.
                                                  –1
                                                  require_once(...) попробуйте, или логика именно на инклюдах?
                                                    +1
                                                    От замены include(_once) на require(_once) — ситуация не меняется, и использование памяти/cpu такое же.
                                                      0
                                                      В вашем комменте был include, без once…
                                                        +2
                                                        Я ещё раз повторюсь что это не играет никакой роли в данной ситуации. Разница между include_once и include — только в том, что include_once не даст добавить в рабочую область скрипта один и тот же скрипт несколько раз. Но это в любом случае не получится, если вы используете классы — будет фатальная ошибка с тем что имя класса уже используется.

                                                        Такая же ситуация с require и require_once — в рамках данной статьи и примера выше — разницы никакой.
                                                          0
                                                          Это я увлекся просто, сорри. Пока писал что в случае класс лоадера это неважно, Вы уже ответили.
                                                –25
                                                проблему решает переход на Python/Ruby/Node etc. Желательно с предзагрузкой кода в мастер-процесс. Ну или даже Go :)
                                                  +7
                                                  очень сомнительное заявление. PHP не настолько медленнее, чтобы из-за этого переезжать на другой язык всей командой разработки. И на тормозящие на этих языках приложения я тоже насмотрелся.
                                                    0
                                                    +1

                                                    я бы посоветовать посмотреть в сторону HHVM уж лучше.
                                                      0
                                                      На самом деле, часто PHP реально быстрее в похожих задачах, чем Python/Ruby
                                                        0
                                                        это решает конкретную проблему повторной компиляции/интерпретации исходного кода и некоторого иного.

                                                        можно оставаться и на PHP, но с учетом понимания его проблем.
                                                        +8
                                                        Акжан, что-то тебя заминусили ) На самом деле тут работает принцип «коней на переправе не меняют». Часто смена платформы это долго дорого и неэффективно, приходится оптимизировать то что есть. А говнокод и на python/ruby тоже случается, особенно если некто считает что этот язык/платформа тупо быстрее PHP и можно делать что левой задней ноге вздумается.
                                                        +3
                                                        После оптимизации мы максимально избавились от этих функций где возможно. Данные в файлах мы решили хранить в виде массива, сохранённого через var_export и загружать обратно через с помощью include/require. Таким образом мы смогли задействовать APC режим.

                                                        К сожалению, для данных которые хранятся в memcache, данный подход не работает.
                                                        Просто используйте igbinary.
                                                          0
                                                          Это всё равно сериализация, которая всё равно есть CPU.
                                                          Загрузка конфига, который уже закэширован в виде опкода, сильно быстрее чем любая сериализация, бенчи это показывают.
                                                          Быстрее только бинарный слепок памяти массива, но такого в php пока нет, это к сожалению не C.
                                                            0
                                                            Вы так говорите, как будто загрузка опкода это не CPU.
                                                            Загрузка конфига, который уже закэширован в виде опкода, сильно быстрее чем любая сериализация, бенчи это показывают.
                                                            Буду признателен на ссылку на замеры.
                                                              0
                                                              Самому стало интересно, и не нашёл ни одного теста где сравниваются:
                                                              1. serialize
                                                              2. igbinary
                                                              3. var_export + include
                                                              4. var_export + include с opcache

                                                              если брать первые 3 пункта, то во всех тестах что видел — побеждает igbinary.
                                                              Что на самом деле странно, так как можно было бы подумать что должен побеждать include файла.

                                                              Пора проводить новые тесты :) на пхп 5.5 с opcache включённым.
                                                                0
                                                                Что уж странного? Более простой и специализированный интерпретатор должен работать быстрее.
                                                                  0
                                                                  Когда-то я делал alekciy.livejournal.com/10826.html что собственно и определило лично для меня использование igbinary как стандартного сериализатора данных. Минус, точнее неудобство, только одно. Нельзя глазами заглянуть в результат сериализации и посмотреть, что там. Бывало нужно, хотя и крайне редко.
                                                            0
                                                            Советы довольно интересные, спасибо.
                                                            Расскажите и про xhProf, нужная тема.
                                                              +5
                                                              Вы утверждаете в статье, что чисто загрузка 12Мб кода из APC опкод кеша у вас занимало больше 300мс? У вас точно не было тяжёлой инициализации с запросами в БД, криво настроенного APC (размер кеша, проверка mtime на каждый запрос etc), тяжёлого автолоадера с просмотром файловой системы и прочей связанной печали? Потому как такого порядка потребление CPU фантастическое и больше опытного на порядок.
                                                                +1
                                                                Конечно же есть запросы к БД, к мемкеш и тп. Их в данный момент не меняли — т.е. график показывает только изменение при вышеуказанной оптимизации… А оптимизация конечно идёт по всем фронтам. В данной статье представлен только один из них.
                                                                  0
                                                                  Проблема в том, что кмк вы исправляете последствие проблемы, а не причину. PHP очень эффективно работает с опкод кешем (особенно opcache), и ломать логику приложения (см. примеры 1-3) в этом случае — бессмысленная трата времени.
                                                                  Вы пробовали провести чистый бенчмарк данной проблемы, без вашего кода?
                                                                    0
                                                                    Почитайте, пожалуйста, мои комментарии в данной теме. В любом случае подключение лишнего кода в «рабочую область процесса» — замедляет общее выполнение процесса. Opcache уменьшает проблему, но не избавляет от неё полностью: вам всё равно надо выделять память на «структуру класса», загрузку массива, тратить CPU на всё это, плюс на выполнение опкодов из opcache.

                                                                    Как раз таки примеры 1-3 это примеры того, как можно небольшими усилиями, исправить ситуацию в архитектуре приложения, не сильно меняя саму архитектуру.

                                                                  0
                                                                  Поддерживаю. Данное поведение более характерно для промаха кеша.
                                                                  @Для автора:
                                                                  * Предположим у вас opcode cache выставлен на 64 МиБ, а занято 20 МиБ. Значит ли это что кеша вам хватает? Вовсе нет, он может быть сильно фрагментирован и большие файлы в него не попадают и вызывают промах кеша. И в этой ситуации конечно же ваша оптимизация спасает. но проще увеличить память.
                                                                  * Я тут когда писал пост. Посмотрите картинку вначале и выводы в конце.
                                                                  –1
                                                                  в тему снижения потребления памяти просится уменьшение времени жизни объектов, но сработает далеко не каждом коде. То есть если объект создается в начале запроса, а первое использование — где то в конце, то он все это время жрет память. Умножаем время жизни на число одновременных запросов — получаем масштаб бедствия. Переносим создание объекта поближе к его первому использованию, уменьшаем время*объем используемой памяти.
                                                                  Повторюсь, сработает далеко не везде, но вот на эту тему было бы действительно интересно посмотреть.
                                                                    +1
                                                                    Переносим создание объекта поближе к его первому использованию, уменьшаем время*объем используемой памяти.

                                                                    Для этого и придумали IoC контейнеры, которые берут на себя всю рутину по созданию экземляров классов на себя и инициализируют их только по требованию. Продвинутые IoC контейнеры даже могут обернуть автоматически ваши классы в проки для инициализации только если вы начали их использовать. Так же некоторые умеют компилировать контейнер за счет чего, при наличии опкэша, оверхэда от их использования практически не ощущается.
                                                                      0
                                                                      спасибо! ушел расширять горизонты.
                                                                    0
                                                                    >>Данные в файлах мы решили хранить в виде массива, сохранённого через var_export
                                                                    Абсолютно верно, я так делаю уже лет 10. Это надо крупными буквами писать везде, где идёт описание функций serialize или .ini, что не надо их использовать где попало. Особенно умиляли ресурсоёмкие парсинги ini файлов или конфигов xml, вот это изврат.
                                                                    Да, жаль что в APC/memcache нельзя положить и достать слепок массива как есть, без serialize и без var_export. Во всяких новомодных hhvm/hack/phpng такой возможности так и не появилось?
                                                                      +1
                                                                      HHVM — это виртульная машина языка Hack, а PHPNG (PHP7) ещё не существует. Но, отвечая на ваш вопрос, и Hack, и PHP7 — языки высокого уровня, они такими вещами, как манипуляции слепками высоких структур прямо в памяти, не занимаются.
                                                                      +1
                                                                      Кроме xhprof порекомендую ещё blackfire.io, очень удобно и наглядно
                                                                        0
                                                                        +1

                                                                        Оч клево, пара apt-get install + ставим расширение для хрома и на нужной для профилирования странице кликаем по иконке этого расширения. Получаем красивые и удобные отчеты:

                                                                        Пример

                                                                        0
                                                                        Интересно совпали цвета на втором графике )

                                                                        Only users with full accounts can post comments. Log in, please.