Коротко: профилирование — поиск узких мест по времени и памяти; оптимизировать нужно по данным, а не наугад. Инструменты и приёмы для PHP.

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

Для начала давайте разберемся что вообще такое профилирование и для чего оно нам нужно и в какой ситуации нам его надо применять.

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

И вот, чтобы понять где конкретно находится проблема или проблемы - нам нужно профилировать код.

При профилировании кода есть несколько правил, которые нужно строго соблюдать, иначе у вас ничего не получится и вы уйдете вообще не в ту степь абсолютно.

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

- Профилирование кода должно производиться на том же объеме данных, том же окружении или максимально близком к тому, где воспроизводится проблема - например у Symfony в .env файле есть переключатель dev/prod окружения. Очень важно во время профилирования переключаться в prod режим, чистить и прогревать кэш и только после этого приступать к профилированию. Иначе вы опять же получите ложные диагностические данные и точно так же будете строить ложные теории.

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

- Также часто проблемными местами являются циклы, которые могут выполнять излишнюю или ненужную работу. Они могут быть написаны не оптимально или могут быть условно бесконечными ( когда вместо 100 итераций осуществляется 1 000 000 итераций из-за неверной логики внутри цикла ).

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

- Не стоит пренебрегать подходом "пристального взгляда" - возможно ошибка банальная и кроется где-то на поверхности, не стоит сразу тратить целую тучу времени на детализацию - для начала стоит просто внимательно прочитать код.

Метод "пристального взгляда"

Частенько бывает так, что проблема кроется где-то очень на поверхности и достаточно просто внимательно прочитать код. Например в моей практике был случай, что разработчик в одном из методов оставил бесконечный цикл. Так как логика была очень ветвестая и сложная, а система написана с участием очередей, то QA эту проблему не выявило, просто не попало во время тестирования в этот очень узкий сценарий.

Соответственно локально все было хорошо, а при выводе в прод через какой-то промежуток времени мы увидели, что идет какая-то странная деградация пиками на графиках. Что происходило в реальности. Код примерно в 3% случаев заходил в этот сценарий, внутри бесконечного цикла выжирал всю оперативу, падал и заново ставил сам себя в очередь на обработку. В итоге мы получали на графиках очень интересную картинку деградации ресурсов сервера.

Провели дополнительное ревью всех задач, которые входили в релиз и обнаружили бесконечный цикл, который был реализован через while (true) конструкцию с выходом по определенному условию. И получалось так, что выход из цикла происходил не в 100% случаев, а лишь в 97% случаев.

Что будет, если профилировать, не зная функционала?

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

Поэтому по возможности стоит конечно, либо это доверять тем, кто уже работал с данным кодом, либо предварительно детально его изучить, почитать ТЗ и проясниться как он вообще должен работать.

Почему нельзя профилировать с включённым XDebug?

Тут всё довольно тривиально - любые отладчики потребляют ресурсы сервера зачастую даже в большем количестве, чем сами скрипты, которые выполняются. Соответственно, например вы будете искать утечки оперативной памяти, а XDebug будет ее потреблять больше, чем сама утечка. Причем потребляет он ее не равномерно, а по какой-то своей логике. Соответственно вы не сможете адекватно составить список мест где сколько памяти потребляется и соответственно будете строить гипотезы и исследовать куски кода, которые скорее всего работают нормально. А как только отключите XDebug - все сразу встанет на свои места.

Профилировать в том же окружении и на том же объеме данных

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

Во-первых, скорее всего вы будете работать во фреймворке, а каждый фреймворк имеет разные настройки в зависимости от окружения dev / prod. Это как и настройки кэширования, так и настройки БД и очень много чего еще. Это может играть роль при профилировании в части скорости работы, а так же объеме потребляемой памяти.

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

В-третьих, совсем желательно, чтобы и данные были бы с прода, залитые в локальную базу данных. Тут может возникнуть проблема с it-sec, потому что они могут воспротивиться предоставлять доступ к боевым данным, но в этом случае можно запросить обфусцированные данные. Обфускация - это такой процесс, когда структура данных остается той же самой, но при этом данные немного все же отличаются. Например может быть изменены символы в имени ( но не количество символов ), номер карты, сумма операции и прочие параметры. За счет такого подхода - вы максимально приблизитесь к реальной боевой среде и повысите свои шансы на более быстрый поиск точки / точек проблемы.

Как профилировать код?

Скорость работы

Самым простым способом профилирования скриптов в плане скорости работы - это использование функции microtime() встроенной в язык php.

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

Предположим, что у нас есть какой-то вот такой вот скрипт ( естественно он не работает, это просто пример ):

<?php

class SomeCliCommand
{
    private $currencyRateModel, $currencyModel, $merchantModel, $merchantConfigurationModel, $currencyService;

    public function exec()
    {
        $currencyRates = $this->currencyRateModel->find();

        foreach ($currencyRates as $currencyRate) {
            $currencyPair = $this->currencyModel->getCurrencyPair($currencyRate);
            $merchants = $this->merchantModel->getAllMerchants();
            foreach ($merchants as $merchant) {
                $configurations = $this->merchantConfigurationModel->getByMerchant($merchant, $currencyPair, $currencyRate);
            }

            $this->currencyService->prepareCurrencyRatesWithMarkups($currencyPair, $currencyRate, $merchants, $configurations);
        }
    }
}

Мы знаем, что на небольшом объеме данных мы всё проверяли и код работает. Но в проде на большом объеме данных код начинает деградировать по скорости работы очень сильно. Первым делом нам нужно понять в каком именно месте происходит основная деградация кода. Для этого расставим по коду логирование с использованием microtime() следующим образом ( сейчас мы не обращаем внимание на форматирование кода, вся отладка смещена максимально влево для того, чтобы ее можно было потом легко отыскать глазами в коде и удалить ):

<?php

class SomeCliCommand
{
    private $currencyRateModel, $currencyModel, $merchantModel, $merchantConfigurationModel, $currencyService;

    public function exec()
    {
$a = microtime(true);
        $currencyRates = $this->currencyRateModel->find();
file_put_contents('./log.txt', sprintf('1: %s'.PHP_EOL, microtime(true) - $a), FILE_APPEND);

        foreach ($currencyRates as $currencyRate) {
$a = microtime(true);
            $currencyPair = $this->currencyModel->getCurrencyPair($currencyRate);
file_put_contents('./log.txt', sprintf('2: %s'.PHP_EOL, microtime(true) - $a), FILE_APPEND);
$a = microtime(true);
            $merchants = $this->merchantModel->getAllMerchants();
file_put_contents('./log.txt', sprintf('3: %s'.PHP_EOL, microtime(true) - $a), FILE_APPEND);
$a = microtime(true);
            foreach ($merchants as $merchant) {
                $configurations = $this->merchantConfigurationModel->getByMerchant($merchant, $currencyPair, $currencyRate);
            }
file_put_contents('./log.txt', sprintf('4: %s'.PHP_EOL, microtime(true) - $a), FILE_APPEND);
$a = microtime(true);

            $this->currencyService->prepareCurrencyRatesWithMarkups($currencyPair, $currencyRate, $merchants, $configurations);
file_put_contents('./log.txt', sprintf('5: %s'.PHP_EOL, microtime(true) - $a), FILE_APPEND);
        }
    }
}

После запуска скрипта в файле log.txt мы получим примерно такие данные:

1: 0.00017619132995605
2: 0.00087404251098633
3: 0.10452103614807
4: 0.43679308891296
5: 2.0005280971527
2: 0.00093698501586914
3: 0.11457705497742
4: 1.0363759994507
5: 2.0004959106445
2: 0.00088787078857422
3: 0.1244900226593
4: 1.6367290019989
5: 2.000559091568
2: 0.00094509124755859
3: 0.13469290733337
4: 2.2366151809692
5: 2.0005509853363
2: 0.00094890594482422
3: 0.14480495452881
4: 2.8366689682007
5: 2.000519990921
2: 0.0009150505065918
3: 0.15450501441956
4: 3.4365401268005
5: 2.0006878376007
2: 0.00095605850219727
3: 0.16457486152649
4: 4.0365629196167
5: 2.0005469322205
2: 0.00095105171203613
3: 0.17456483840942
4: 4.6367800235748
5: 2.0005588531494
2: 0.00094914436340332
3: 0.18463087081909
4: 5.2365169525146
5: 2.0005400180817
2: 0.00095009803771973
3: 0.1945698261261
4: 5.8369708061218
5: 2.0007538795471

Как проанализировать эти данные.

Для начала смотрим п.1 - он у нас встречается всего один раз в коде и отрабатывает довольно быстро - значит с ним вряд ли есть какие-либо проблемы.

Далее у нас начинаются циклические операции - значит нам надо посмотреть наличие деградации на периоде цикла.

Смотрим первое, среднее (примерно из центра) и последнее значения по п.2 - это будут значения:

0.00087404251098633
0.00094890594482422
0.00095009803771973

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

Переходим к пункту 3 и смотрим его значения по тому же алгоритму:

0.10452103614807
0.14480495452881
0.1945698261261

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

Переходим к пункту 4 и смотрим его значения по тому же алгоритму:

0.43679308891296
2.8366689682007
5.8369708061218

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

Но на этом мы еще не радуемся и не прекращаем анализ и переходим к пункту 5:

2.0005280971527
2.000519990921
2.0007538795471

Мы не видим, что здесь нет особой деградации, но работает оно медленно. Просто медленно, но без деградации от объемов.

Итого в сухом остатке обращаясь к скрипту мы понимаем, что у нас есть проблемы по пунктам 3, 4 и 5. Теперь ищем эти места в нашем скрипте:

PHP-код CLI-команды с ручным замером времени через microtime и логированием в файл для профилирования узких мест

И теперь смотрим на скрипт и думаем что мы можем с этим сделать, чтобы исправить положение.

Смотрим на строчку 18 и мы понимаем, что у нас в цикле идет обращение к БД каждый раз с одним и тем же запросом - мы получаем список всех мерчантов каждый раз в цикле снова и снова, хотя это достаточно сделать всего 1 раз и потом просто использовать его единожды и всё. Соответственно мы выносим 18 строку за пределы цикла. Таким образом наш код становится следующего вида:

<?php

class SomeCliCommand
{
    private $currencyRateModel, $currencyModel, $merchantModel, $merchantConfigurationModel, $currencyService;

    public function exec()
    {
        $currencyRates = $this->currencyRateModel->find();

        $merchants = $this->merchantModel->getAllMerchants();
        foreach ($currencyRates as $currencyRate) {
            $currencyPair = $this->currencyModel->getCurrencyPair($currencyRate);
            foreach ($merchants as $merchant) {
                $configurations = $this->merchantConfigurationModel->getByMerchant($merchant, $currencyPair, $currencyRate);
            }

            $this->currencyService->prepareCurrencyRatesWithMarkups($currencyPair, $currencyRate, $merchants, $configurations);
        }
    }
}

Можно сказать, что этот пункт мы поправили - осталось еще два. Давайте рассмотрим строки 21-23 - там мы видим цикл, который перебирает список мерчантов и для каждого мерчанта обращается к БД. Скорее всего там может быть какой-то запрос вида

SELECT * FROM merchant_configuration WHERE merchant_id = $merchantId AND currency_pair_id = $currencyPair AND currency_rate_id = $currencyRate;

Что мы можем сделать в данной ситуации?

1) мы можем переделать запрос с "WHERE merchant_id =" на "WHERE merchant_id IN ()" и таким образом как минимум мы либо сократим количество итераций ( если количество мерчантов большое, то нельзя пихать всех сразу ), либо, если количество мерчантов не большое, то сократим вообще до одного запроса вместо десятков, сотен и тысяч.

2) мы должны проверить существует ли для таблицы merchant_configuration составной индекс по полям currency_pair_id, currency_rate_id, merchant_id. Если таблица большая ( а она скорее всего большая ) и в ней нет составного индекса для такого запроса - то запрос будет изначально очень медленный и еще и будет деградировать и мешать другим процессам системы. Как правильно создавать составные индексы мы рассмотрим в другой статье - сейчас нам важно просто убедиться, что он есть и что он корректный. Если его нет или он не корректный - то поправить или создать корректный.

Теперь наш код примет вот такой вот вид:

<?php

class SomeCliCommand
{
    private $currencyRateModel, $currencyModel, $merchantModel, $merchantConfigurationModel, $currencyService;

    public function exec()
    {
        $currencyRates = $this->currencyRateModel->find();

        $merchants = $this->merchantModel->getAllMerchants();
        foreach ($currencyRates as $currencyRate) {
            $currencyPair = $this->currencyModel->getCurrencyPair($currencyRate);
            $configurations = $this->merchantConfigurationModel->getByMerchant($merchants, $currencyPair, $currencyRate);
            $this->currencyService->prepareCurrencyRatesWithMarkups($currencyPair, $currencyRate, $merchants, $configurations);
        }
    }
}

И у нас остается последний пятый пункт, где мы вызываем метод prepareCurrencyRatesWithMarkups() из сервиса валют, в котором нет деградации, но он просто медленно работает. Здесь всё очень просто - мы открываем этот метод и начинаем там делать всё то же самое, что делали здесь.

Таким образом мы разматываем всю цепочку от самого начала и до самого конца.

После того как мы размотали всю цепочку и все поправили - делаем итоговые замеры скорости работы ( полный прогон ). Если результаты нас удовлетворяют - то всё хорошо, заливаем фиксы в род и радуемся жизни. Если результаты нас не удовлетворяют - то в этом случае необходимо пересматривать архитектуру приложения и либо внедрять кэширование, либо очереди, либо масштабироваться горизонтально или еще как-то - тут уже всё зависит от ситуации, но это совсем другая история. С уровня кода можно считать, что всё поправлено, что было можно.

Утечки памяти

Утечки памяти ищутся абсолютно по тому же принципу, что и замеры скорости работы скрипта с той лишь разницей, что мы замеряем не время работы, а потребляемую память функцией memory_get_usage(). Функция на вход принимает bool аргумент. Если передать true - то она будет показывать максимально аллоцированный объем памяти за всё время работы скрипта, а если ничего не передать или передать false - то будет показывать используемую память в текущий момент времени. Пользоваться можно как удобнее, я покажу способ как удобнее делать лично мне.

При диагностике утечек памяти необходимо всегда помнить один очень важный нюанс. Мы не стремимся снизить количество потребляемой памяти до нуля. В подавляющем большинстве случаев это невозможно. Эту работу стоит делить на 2 подхода. Первый - устранение утечек и замеры. Второй - на основании замеров либо ищем возможности сократить количество потребляемой памяти либо расширяем железо. Нужно понимать, что скрипты потребляют память и это нормально, если они это делают в разумных пределах. Но при этом не стоит переусердствовать в оптимизации. Если вы понимаете, что вы уже довольно ощутимо соптимизировались, но при этом все равно постоянно находитесь у верхних границ объема потребляемой памяти и при этом утечек нет - то значит нужно расширить железо на сервере ( сколько бы там админ ни возмущался ( если вдруг будет возмущаться ) ).

Утечки памяти возникают в основном при работе внутри очередей и демонов, которые работают беспрерывно во времени ( не так как страница загрузилась и закончила работу ). Например у вас есть демон, который запущен в бесконечном цикле и который наблюдает за появлением в БД новых записей раз в секунду. В нем могут быть утечки памяти - и это определяется по тому признаку, что он аллоцирует память и не отпускает ее для прочих процессов. То есть демон запустился, отработал свою полезную нагрузку, завершил работу, но при этом не отпустил память, которую использовал в процессе работы. Это называется утечкой. А если ваш скрипт просто потребляет много памяти - это НЕ утечка. Это тоже стоит проверить и оптимизировать возможно, но это не утечка. Мы далее рассмотрим оба варианта.

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

Первым мы рассмотрим именно утечку памяти на примере демона (утечка):

<?php

$contents = [];

$i = 0;
$a = memory_get_usage(true);
while (true) {
    if (++$i % 250000 === 0) {
        $char = readline(PHP_EOL . 'stop?' . PHP_EOL);
        file_put_contents('./log.txt', sprintf($i.': %s'.PHP_EOL, memory_get_usage(true) - $a), FILE_APPEND);
        if ($char === 'y') break;
    }

    $contents[] = file_get_contents('../1.json');
}

Что делает скрипт... Запуская этот скрипт через консоль мы запускаем "как бы демона", который в бесконечном цикле осуществляет какую-то полезную работу. В данном случае его полезная работа заключается в том, чтобы за 250 000 итераций прочитать файл 250 000 раз и сложить его данные в массив (а после например отправить данные кому-то на почту в виде CSV файлика). То есть после того как он обработает 250 000 итераций и отправит их на почту - они ему перестают быть нужны. Но посмотрим, что происходит с памятью во время работы данного скрипта. Раз в 250 000 итераций он останавливается и спрашивает "остановиться или нет?" и логирует количество потребляемой памяти. Посмотрим что он залогирует например на 4 итерациях по 250 000 штук:

250000: 236982272 byte (226 Mb)
500000: 473960448 byte (452 Mb)
750000: 719327232 byte (686 Mb)
1000000: 947916800 byte (904 Mb)

Мы видим, что с каждой итерацией по 250 000 чтений файла - мы постоянно прибавляем объем аллоцированной памяти и не отпускаем ее и наш массив с данными всё пухнет. И так будет продолжаться до тех пор пока мы не упадем с ошибкой, что память исчерпана.

Как же это можно поправить:

<?php

$contents = [];

$i = 0;
$a = memory_get_usage(true);
while (true) {
    if (++$i % 250000 === 0) {
        $char = readline(PHP_EOL . 'stop?' . PHP_EOL);
        file_put_contents('./log.txt', sprintf($i.': %s'.PHP_EOL, memory_get_usage(true) - $a), FILE_APPEND);
        unset($contents);
        $contents = [];
        if ($char === 'y') break;
    }

    $contents[] = file_get_contents('../1.json');
}

Мы добавляем unset и пересоздаем переменную $contents между file_put_contents и проверкой $char. Запускаем скрипт снова и смотрим результаты:

250000: 236982272
500000: 236982272
750000: 236982272
1000000: 236982272

Да, мы аллоцировали 226 Mb из оперативной памяти, но этот показатель перестал расти с количеством итераций. То есть у нас получилось оптимизировать работу скрипта и избавиться от утечки памяти.

Наиболее актуальны утечки памяти именно в демонах и длительных cli командах, по той причине, что они аллоцируют ресурсы сервера и не отпускают их либо до тех пор пока не свалятся в ошибку либо до завершения своей работы. В PHP-FPM ( когда через браузер ) эта проблема стоит не так остро, но зависит от объемов утечки памяти. Если утечка небольшая - то либо скрипт выполнится быстрее, чем утечет достаточное количество для выброса ошибки, либо скрипт будет убит по таймауту. Если утечка большая - то там конечно же тоже будет ошибка падения по памяти. Но в демонах - это в разы критичнее. Даже крохотная утечка в демоне может долго-долго капать и в итоге скрипт будет падать и может аффектить бизнес-логику и прочие системы, оставляя не валидные данные в БД например.

Слишком большая аллокация памяти

Теперь поговорим про слишком большую аллокацию памяти и как с ней бороться. Большая часть разработчиков сталкивалась с ошибкой вида "Fatal error: Allowed memory size of XXX bytes exhausted" - именно эта ошибка и говорит о том, что превышен лимит использования оперативной памяти.

Очень многие сайты, рекомендуют решать эту проблему перенастройкой php.ini файла и параметра memory_limit или же предлагают зашивать прямо в код строки вида ini_set('memory_limit', '100M'). Я считаю подобные решения недопустимыми в 98% случаев ( особенно в демонах и cli ). Потому что подобные решения применяют в основном новички и подобным образом проблема не решается, а только скрывается. Это аналогично тому, что вы пришли к доктору со словами "у меня аппендицит болит, я помираю!", а он вам отвечает "я вам ща укол лошадиного транквилизатора поставлю и вы ничего чувствовать не будете. помрёте правда к утру, но за то ничего чувствовать не будете, болеть не будет". Вот примерно так же и тут. Такими способами - вы сильно скрываете очень большую проблему.

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

Как же с этим бороться. А бороться с одной стороны не сложно, а с другой стороны сложно. Алгоритм действий примерно такой:

1) как и с утечками - сделать контрольные замеры
2) как и с утечками - сделать замеры внутри кода расставив диагностику и выявить те точки, где память уходит в пики по потреблению
3) оптимизировать

В подавляющем большинстве случаев проблемы обнаружатся в вашем коде ( и это хорошо ), на втором месте по популярности - это сторонние библиотеки, встроенные компоненты фреймворков и CMS ( это чуть хуже ) и уже реже - это встроенные функции php ( это прям плохо ).

Давайте рассмотрим каждый из случаев по отдельности.

Проблемы в вашем коде - это наиболее просто и быстро устраняемые проблемы. Часто они связаны с неоптимальными выборками из БД, когда например вы вместо выборок чанками - делаете огромную выборку всех объектов из БД и пытаетесь держать это в переменной. Также часто встречается не оптимальная работа с циклами, когда, например вы сделали какую-то крупную выборку и пытаетесь ее прожевать в цикле без очистки переменных и не освобождая память. Здесь каждую проблемную точку нужно искать и индивидуально принимать решение, которое чаще всего сводится к тому, чтобы немного оптимизировать код. Также частой причиной превышения лимита памяти является "переборщ" с ООП. Объекты при прочих равных весят намного больше, чем массивы, особенно если у них идет какое-то глубокое наследование через 100500 паттернов абсолютно ненужных зачастую. Поэтому будьте аккуратны с ООП, чтобы не заООПшить код насмерть.

Проблемы в сторонних библиотеках, встроенных компонентах фреймворков и CMS - здесь уже вопрос решается несколько сложнее, потому что данные компоненты ставятся через composer и их изменить не так то просто, особенно, если вы часто обновляетесь. Автор той или иной библиотеки не всегда адекватно может реагировать на критику и даже если вы внесёте изменения в саму библиотеку и по всем правилам оформите баг и мерж реквест - то совсем не факт, что владелец репозитория его быстро посмотрит, примет и задеплоит новую версию. А рабочий код вам нужен уже сейчас. Поэтому будет вставать дилемма, либо вносить изменения у себя локально, держать vendor в репозитории и держать в памяти, что обновлять текущие-зафиксенные библиотеки нельзя, либо искать какие-то другие обходные пути, например писать свою собственную библиотеку, либо форкаться от исходной библиотеки другого автора, либо искать другую библиотеку. Но есть и хорошие новости - обычно те библиотеки и готовые компоненты, которые текут по памяти - широко известны и об этом много где написано. Поэтому, если при выборе библиотеки обратить на это внимание - то скорее всего проблемы удастся избежать.

Проблемы во встроенных функциях php - это самый плохой случай, потому что с этим вы ничего самостоятельно сделать не сможете. Если какая-то встроенная в язык функция течет по памяти - то исправить это смогут только разработчики языка в следующем релизе в лучшем случае и если им об этом сообщат и если они смогут это поправить. Поэтому рассчитывать на скорое решение в данном случае вообще не приходится от слова совсем. Единственно верный для вас в этом случае путь будет - отказаться от использования конкретной функции и придумывать другие методы разработки конкретного функционала.


Надеюсь вам понравилась данная статья и поможет вам с профилированием и отладкой вашего кода. Подписывайтесь на мой ТГ-канал, чтобы не пропустить новые статьи и обновления по ссылке.