company_banner

PHP, почём абстракции для народа?


    Joy: What is going on?
    Sadness: We’re abstracting! There are four stages. This is the first. Non-objective fragmentation!
    Bing Bong: Alright, do not panic. What is important is that we all stay together. [suddenly his abstract arm falls off]
    Joy: Oh! [Sadness and Joy start falling apart too]
    Sadness: We’re in the second stage. We’re deconstructing! [as Bing Bong falls to pieces]
    Bing Bong: I can’t feel my legs! [picks one leg up] Oh, there they are.
    © мультфильм Inside Out


    Все любят писать красивый код. Чтобы абстракции, лямбды, SOLID, DRY, DI и т.д. и т.п. В этой статье я хочу исследовать, во сколько обходится это всё с точки зрения производительности и почему.

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

    Дисклеймер: Эта статья ни в коем случае не должна рассматриваться как призыв писать плохой код. Лучше всего, если вы заранее настроитесь сказать после прочтение «Прикольно! Теперь я знаю, как оно там внутри. Но, конечно же, не буду это использовать». :)

    Задача:

    1. Дан текстовый файл.
    2. Разобьём его по строкам.
    3. Обрежем пробелы слева и справа
    4. Отбросим все пустые строки.
    5. Все не единичные пробелы заменим единичными («A B C»->«A B C»).
    6. Строки, в которых более 10 слов, по словам перевернём задом наперёд («An Bn Cn»->«Cn Bn An»).
    7. Посчитаем, сколько раз встречается каждая строка.
    8. Выведем все строки, которые встречаются более N раз.

    В качестве входного файла по традиции возьмём php-src/Zend/zend_vm_execute.h на ~70 тысяч строк.

    В качестве среды исполнения возьмём PHP 7.3.6.
    На скомпилированные опкоды посмотрим тут https://3v4l.org.

    Замеры будем производить следующим образом:

    // объявление функций и классов
    $start = microtime(true);
    
    ob_start();
    for ($i = 0; $i < 10; $i++) {
        // тут наш код
    }
    ob_clean();
    
    echo "Time: " . (microtime(true) - $start) / 10;

    Подход первый, наивный


    Напишем простой императивный код:

    $array = explode("\n", file_get_contents('/Users/rjhdby/CLionProjects/php-src/Zend/zend_vm_execute.h'));
    $cache = [];
    
    foreach ($array as $row) {
        if (empty($row)) continue;
        $words = preg_split("/\s+/", trim($row));
        if (count($words) > 10) {
            $words = array_reverse($words);
        }
        $row = implode(" ", $words);
        if (isset($cache[$row])) {
            $cache[$row]++;
        } else {
            $cache[$row] = 1;
        }
    }
    
    foreach ($cache as $key => $value) {
        if ($value > 1000) {
            echo "$key : $value" . PHP_EOL;
        }
    }

    Время выполнения ~0.148с.

    Тут всё просто и разговаривать особо не о чем.

    Подход второй, процедурный


    Отрефакторим наш код и вынесем элементарные действия в функции.
    Постараемся придерживаться принципа единственной ответственности.

    Портянка под спойлером.
    function getContentFromFile(string $fileName): array
    {
        return explode("\n", file_get_contents($fileName));
    }
    
    function reverseWordsIfNeeded(array &$input)
    {
        if (count($input) > 10) {
            $input = array_reverse($input);
        }
    }
    
    function prepareString(string $input): string
    {
        $words = preg_split("/\s+/", trim($input));
        reverseWordsIfNeeded($words);
        return implode(" ", $words);
    }
    
    function printIfSuitable(array $input, int $threshold)
    {
        foreach ($input as $key => $value) {
            if ($value > $threshold) {
                echo "$key : $value" . PHP_EOL;
            }
        }
    }
    
    function addToCache(array &$cache, string $line)
    {
        if (isset($cache[$line])) {
            $cache[$line]++;
        } else {
            $cache[$line] = 1;
        }
    }
    
    function processContent(array $input): array
    {
        $cache = [];
        foreach ($input as $row) {
            if (empty($row)) continue;
            addToCache($cache, prepareString($row));
        }
        return $cache;
    }
    
    printIfSuitable(
        processContent(
            getContentFromFile('/Users/rjhdby/CLionProjects/php-src/Zend/zend_vm_execute.h')
        ),
        1000
    );


    Время выполнения ~0.275с… WTF!? Разница почти в 2 раза!

    Посмотрим, что из себя представляет функция PHP с точки зрения виртуальной машины.

    Код:

    $a = 1;
    $b = 2;
    $c = $a + $b;

    Компилируется в:

    line     #* E I O op                           fetch          ext  return  operands
    -------------------------------------------------------------------------------------
       2     0  E >   ASSIGN                                                   !0, 1
       3     1        ASSIGN                                                   !1, 2
       4     2        ADD                                              ~5      !0, !1
             3        ASSIGN                                                   !2, ~5
    

    Давайте вынесем сложение в функцию:

    function sum($a, $b){
        return $a + $b;
    }
    
    $a = 1;
    $b = 1;
    
    $c = sum($a, $b);

    Такой код скомпилируется в два набора опкодов: один для корневого пространства имён, а второй для функции.

    Корень:

    line     #* E I O op                           fetch          ext  return  operands
    -------------------------------------------------------------------------------------
       2     0  E >   ASSIGN                                                   !0, 1
       3     1        ASSIGN                                                   !1, 1
       5     2        NOP
       9     3        INIT_FCALL                                               'sum'
             4        SEND_VAR                                                 !0
             5        SEND_VAR                                                 !1
             6        DO_FCALL                                      0  $5
             7        ASSIGN                                                   !2, $5

    Функция:

    line     #* E I O op                           fetch          ext  return  operands
    -------------------------------------------------------------------------------------
       5     0  E >   RECV                                             !0
             1        RECV                                             !1
       6     2        ADD                                              ~2      !0, !1
             3      > RETURN                                                   ~2

    Т.е. даже если просто по опкодам посчитать, то каждый вызов функции добавляет 3 + 2N опкодов, где N — количество передаваемых аргументов.

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

    Грубая прикидка по нашему отрефакторенному коду даёт такие цифры (помним про 70 000 итераций).
    Количество «дополнительных» исполненных опкодов: ~17 000 000.
    Количество переключений контекста: ~280 000.

    Подход третий, классический


    Особо не мудрствуя, обернём все эти функции классом.

    Простыня под спойлером
    class ProcessFile
    {
        private $content;
        private $cache = [];
    
        function __construct(string $fileName) {
            $this->content = explode("\n", file_get_contents($fileName));
        }
    
        private function reverseWordsIfNeeded(array &$input) {
            if (count($input) > 10) {
                $input = array_reverse($input);
            }
        }
    
        private function prepareString(string $input): string {
            $words = preg_split("/\s+/", trim($input));
            $this->reverseWordsIfNeeded($words);
            return implode(" ", $words);
        }
    
        function printIfSuitable(int $threshold) {
            foreach ($this->cache as $key => $value) {
                if ($value > $threshold) {
                    echo "$key : $value" . PHP_EOL;
                }
            }
        }
    
        private function addToCache(string $line) {
            if (isset($this->cache[$line])) {
                $this->cache[$line]++;
            } else {
                $this->cache[$line] = 1;
            }
        }
    
        function processContent() {
            foreach ($this->content as $row) {
                if (empty($row)) continue;
                $this->addToCache( $this->prepareString($row));
            }
        }
    }
    
    $processFile = new ProcessFile('/Users/rjhdby/CLionProjects/php-src/Zend/zend_vm_execute.h');
    $processFile->processContent();
    $processFile->printIfSuitable(1000);
    


    Время выполнения: 0.297. Стало хуже. Не сильно, но заметно. Неужели создание объекта (10 раз в нашем случае) такое затратное? Нууу… Не только в этом дело.

    Давайте посмотрим, как виртуальная машина работает с классом.

    class Adder{
        private $a;
        private $b;
    
        function __construct($a, $b) {
            $this->a = $a;
            $this->b = $b;
        }
        function sum(){
            return $this->a + $this->b;
        }
    }
    
    $a = 1;
    $b = 1;
    $adder = new Adder($a, $b);
    $c = $adder->sum();

    Тут будет три набора опкодов, что логично: корень и два метода.

    Корень:

    line     #* E I O op                 fetch          ext  return  operands
    ---------------------------------------------------------------------------
       2     0  E >   NOP
      16     1        ASSIGN                                         !0, 1
      17     2        ASSIGN                                         !1, 1
      18     3        NEW                                    $7      :15
             4        SEND_VAR_EX                                    !0
             5        SEND_VAR_EX                                    !1
             6        DO_FCALL                            0
             7        ASSIGN                                         !2, $7
      19     8        INIT_METHOD_CALL                               !2, 'sum'
             9        DO_FCALL                            0  $10
            10        ASSIGN                                         !3, $10

    Конструктор:

    line     #* E I O op                 fetch          ext  return  operands
    ---------------------------------------------------------------------------
       6     0  E >   RECV                                   !0
             1        RECV                                   !1
       7     2        ASSIGN_OBJ                                     'a'
             3        OP_DATA                                        !0
       8     4        ASSIGN_OBJ                                     'b'
             5        OP_DATA                                        !1
       9     6      > RETURN                                         null

    Метод sum:

    line     #* E I O op                 fetch          ext  return  operands
    ---------------------------------------------------------------------------
      11     0  E >   FETCH_OBJ_R                            ~0      'a'
             1        FETCH_OBJ_R                            ~1      'b'
             2        ADD                                    ~2      ~0, ~1
             3      > RETURN                                         ~2

    Ключевое слово new фактически преобразуется в вызов функции (строки 3-6).
    Она создаёт экземпляр класса и вызывает на нем конструктор с переданными параметрами.

    В коде же методов нам будет интересна работа с полями класса. Обратите внимание, что если с обычными переменными при присвоении используется один простой опкод ASSIGN, то для полей класса всё несколько иначе.

    Присвоение — 2 опкода

       7     2        ASSIGN_OBJ                                     'a'
             3        OP_DATA                                        !0
    

    Чтение — 1 опкод

             1        FETCH_OBJ_R                            ~1      'b'

    Тут следует знать, что ASSIGN_OBJ и FETCH_OBJ_R сильно сложнее и, соответственно, более затратны по ресурсам, чем простой ASSIGN, который, грубо говоря, просто копирует zval из одного куска памяти в другой.

    Опкод Количество строк обработчика (С-код)
    ASSIGN_OBJ 149
    OP_DATA 30
    FETCH_OBJ_R 112
    ASSIGN 26

    Понятно, что такое сравнение очень далеко от корректного, но всё же даёт некоторое представление. Чуть дальше произведу замеры.

    А теперь посмотрим, насколько затратно создание экземпляра объекта. Давайте замерим на одном миллионе итераций:

    class ValueObject{
        private $a;
        function __construct($a) {
            $this->a = $a;
        }
    }
    
    $start = microtime(true);
    
    for($i = 0; $i < 1000000; $i++){
        // $a = $i;
        // $a = new ValueObject($i);
    }
    
    echo "Time: " . (microtime(true) - $start);

    Присвоение переменной: 0.092.
    Инстанциация объекта: 0.889.

    Как-то вот так. Не совсем бесплатно, особенно если много раз.

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

    class ValueObject{
        private $b;
    
        function try($a) {
            // Обмен через свойство
            // $this->b = $a;
            // $c = $this->b;
    
            // Обмен через присвоение
            // $b = $a;
            // $c = $b;
    
            return $c;
        }
    }
    
    $a = new ValueObject();
    
    $start = microtime(true);
    
    for($i = 0; $i < 1000000; $i++){
        $b = $a->try($i);
    }
    
    echo "Simple. Time: " . (microtime(true) - $start);


    Обмен через присвоение: 0.830.
    Обмен через свойство: 0.862.

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

    Банальные выводы


    1. В следующий раз, когда вы захотите инстанциировать миллион объектов, задумайтесь, так ли оно вам необходимо. Может, просто массив, а?
    2. Писать спагетти-код ради экономии одной миллисекунды — ну такое. Выхлоп копеечный, а коллеги потом и побить могут.
    3. А вот ради экономии 500 миллисекунд, может быть, иногда и имеет смысл. Главное, не перегибать палку и помнить, что эти 500 миллисекунд, скорее всего, будут сэкономлены только небольшим участком очень горячего кода, и не превращать весь проект в юдоль скорби.

    P.S. Про лямбды в следующий раз. Там интересно. :)
    FunCorp
    318.62
    Разработка развлекательных сервисов
    Share post

    Comments 52

      0
      «Экономия» полусекунды спагетти-кодом обернётся проблемами при поддержке и расширении. Если, конечно вы не пишете код «write once and better never run».
        0

        Не любой код в будущем будет доработан. Вполне возможно, что когда понадобится код доработать — выйдет очередной новомодный фреймворк и задача будет переписать все с нуля.
        Естественно, что your mileage may vary. И писать говнокод не стоит.

          +1
          ИМХО код без использования классов и функций нельзя прямо так назвать говнокодом. Все зависит от контекста :) Писать большой проект одним скриптом явно не стоит. Равно как и бездумно пихать везде ООП тоже.
            0

            Я не говорил, что любой скрипт — говнокод.
            И, да, полностью согласен, что нужно инструменты применять к месту.

          0

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

            +3
            «Экономия» полусекунды спагетти-кодом обернётся проблемами при поддержке и расширении. Если, конечно вы не пишете код «write once and better never run».
            Есть еще вариант. Автогенерация/авторазворачивание кода в более простой. Упрощенно — когда 2 вариант из статьи автоматом конвертится в первый, получается нечто вроде макросов в сях. Убивается два зайца (нет говнокода и нет потерь скорости), но есть нюансы конечно.
              –2
              Поддержка и расширение — это очень абстрактные вещи и большинство статистики собиралось в 80-90 годы, с тех пор появились мощные IDE, выросли возможности языков и появились хорошие по вопросу библиотеки, решающие кучу инфраструктурных проблем.
              Вот у меня был проект сети магазинов, спагети код на функциях, который поддерживался и развивался силами 1.5 разработчиков. Затем его переписали на новый модный фреймворк, с эвентами, команд басами, попытками в ДДД, естественно всё реализовали как микросервисы. На момент моего ухода, команда уже перевалила за 20 человек.
                0
                «перевалила за 20 человек» — это хорошо или плохо? количество разработчиков изменилось только из-за переезда на модный фреймворк или таки были ещё какие-то факторы?
                  0
                  С моей точки зрения, проблема именно в модности подходов, без достаточного опыта работы с ними. Да, появился чуть больший функционал, но не настолько, чтобы команда выросла больше чем на порядок. В то же время много сил начало уходить на тюнинг производительности, ведь то что раньше было вызовом функции, превратилось в запрос к микросервису (прям как в статье). Вынесли склады в отдельный микросервис, для него нужны отдельные разработчики, разработчикам корзины надо меньше знать про склады, но вылазят проблемы взаимодействия команд.
                  Мне как разработчику, стало легче, меньше знаешь, меньше ответственности, больше бюрократии и митингов, откровенно начал забивать и подстаиваться по скорости к остальной команде, взял себе в нагрузку стажёров и курировал проекты с ними. Благо работа была удалённая.
              0
              Могу посоветовать писать с использованием ООП (который проще поддерживать команде и тестировать при грамотном подходе), но затем проверять «тяжелый код» с помощью xhprof.
              Как-то я оптимизировал запросы к базе данных (так как считал, что это замедляет работу тяжелого скрипта), но не получал значительного ускорения работы скрипта. Оказалось, что самое большое замедление давало именно создание объекта ActiveRecord и затем миллионы вызовов __get к виртуальным полям. Да, пришлось переписать конкретно код с объектов на массивы (с потерей возможности работы с сущностями как объектами со своими свойствами), т.е., грубо говоря, вместо $obj->getParentName() писать Something::getParentName($obj), но за счет этого получил троекратное ускорение работы алгоритма.
              Итого, все эти предположения — хороши, но всегда нужно смотреть фактическое употребление памяти и процессорного времени с помощью отладчиков в конкретных алгоритмах и получать приемлемую скорость.
                0

                А почему не $obj->parentName? Публичные поля в объектах все-таки удобнее static-вызовов. Вы же получается все равно заменили функционал ActiveRecord на свой, можно было поля через $this->$name инициализировать по массиву из БД.

                0
                В качестве входного файла по традиции возьмём php-src/Zend/zend_vm_execute.h на ~70 тысяч строк.

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

                +1
                Не менее сильно раздражает когда код раздроблен на миллион файлов, и, если убрать из каждого бойлерплейт, собственно рабочего кода там остаётся на пять-шесть строк. И вот сидишь блуждаешь с зажатым Ctrl, где же всё таки реальная бизнес-логика во всей это мишуре…
                  +2
                  Скажу честно, первый вариант кода ещё и легче читается (по крайней мере для меня) :). Когда логики не очень много, очень легко читать, когда она вся собрана в одном месте. У этого подхода есть много недостатков, в том числе сложность тестирования такого кода, но это можно частично преодолеть тем, чтобы вынести в функции (скорее классы) те участки, которые хочется уметь подменять.

                  Другой вопрос, когда весь проект написан в таком стиле — в этом случае поддержка превращается в ад. Поэтому, ИМХО, совет должен быть немного в другом — не стоит переусложнять код и вносить туда кучу абстракций перед тем, как код действительно стал хотя бы чуточку сложным. Особенно если вы пишете на PHP
                    +1
                    Другой вопрос, когда весь проект написан в таком стиле

                    Как то раз один уважаемый мною писатель выложил на гитхаб движёк своего стендалона. Взыграло во мне чувство бессмысленного и беспощадного альтруизма помочь в его развитии. Форкнул, загрузил, открыл, и волосы у меня начали шевелиться в неожиданных местах — такой адище, что ни в сказке сказать, ни пером описать. Но, как говорится, взялся за гуж… В общем переписал скрипт работы с базой в лучших практиках. Не так, чтоб прям фанатично, но хотя бы по человечески. Дня два ушло только на то, чтобы разобраться, как оно вообще работает и какие граничные условия какими костылями подперты. В ответку прилетело "Это что за фигня? Классы какие-то! Нафиг, нафиг — моему скрипту уже дцать лет, я там каждый костыль знаю и мне так удобно".


                    Не знаю, к чему об этом рассказал. Навеяло. :)

                      0
                      Знаю такую CMS, где коду лет 10-15 уже, по ощущениям (и редким комментариям) всё писано фрилансерами всего мира. Раз в пару лет заглядываю на форум разработчика, и вижу одинокие посты:
                      — «А давайте нормально перепишем?..»
                      — «Иди в *опу, пипл лицензии всёравно покупает всех всё устраивает, самый умный штоле?»
                    0

                    Что там на счёт opcace, вашем тесте он включен?

                      +1

                      Opcache имели в виду? А зачем он там? Время компиляции то нам тут не так чтоб интересно — оно практически никак не сыграет ни для одного из вариантов.

                        0
                        Opcache конечно, отправил сообщение и только потом увидел ошибку.
                        А PHP 8 с JIT уже доступен для тестов? Интересно как там дела обстоят.
                            0

                            Насколько я понимаю принципы работы JIT в PHP, какой-то производительности он добавит, но соотношение не изменится, поскольку накладные расходы на вызов функций никуда не денутся. Мало того, наибольший прирост он даст если просто убрать в одну единственную функцию код изнутри цикла первого примера.

                            0
                            Я думал, что он какую-то оптимизацию опкода проводит, но видимо нет. Проверил ваши примеры, разницы во времени с включенным/выключенным opcache нет.
                          +1
                          Спасибо за статью. Действительно, в «горячих» фрагментах можно и иногда нужно экономить миллисекунды — в наших проектах это позволяло выиграть до 15% производительности кэш, однако возникает проблема — как оформить императивный код таким образом, чтобы он был понятен при обслуживании? Возможно у вас есть какие-то мысли?
                            0

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

                            0
                            explode("\n", file_get_contents($fileName));

                            это просто

                            file($fileName, FILE_IGNORE_NEW_LINES);

                            А тут вы отбрасываете не только пустую строку, но и строку «0»:

                            if (empty($row)) continue;
                              0

                              Не с казал бы, что это принципиально в рамках заданной темы

                                –1
                                Ну городить абстракции над тем что делается из коробки одной функцией, это такое себе. Причем, как заметили выше, в итоге код получился некорректным (отбрасывается «0» как пустая строка).
                                  +1

                                  Вы напомнили мне моего старого преподавателя по начертательной геометрии. Когда у него не было претензий к содержательной части работы — он начинал придираться к тому, что буква А в легенде не по ГОСТу.

                                0
                                А `file($fileName, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES)` еще до кучи и пустые строки отбросит ;)
                                  –1
                                  Во времена php7 файлы в тысячи строк лучше вообще в память не грузить.
                                  Генератор тут выиграет по памяти порядком
                                  public static function readTheRealyBigFile($path) {
                                  	$handle = fopen($path, "r");	
                                  	while(!feof($handle)) {
                                  		yield trim(fgets($handle));
                                  	}	
                                  	fclose($handle);
                                  }
                                  
                                    0
                                    Все так, но не понятно при чем тут php7
                                    Генераторы появились в 5.5
                                      +1
                                      Ключевое слово времена. А у нас 2019 год кончается и 7 актуальнее, да ведь?
                                        +1
                                        Т.е. во «времена» пхп 5.5 память была дешевле что ли? Или может не было файлов в тысячи строк? Не понял посыл.
                                          +1

                                          "Во времена" PHP 5 далеко не везде он был, а во времена PHP7 можно рассчитывать, что 5 уж точно будет :(

                                            +1
                                            Кстати да. Мы вот с 5.3 переехали на 5.6 и далее на 7, а 5.5 вообще пропустили
                                            0
                                            Во-первых, в 7.х вообще работа с памятью получше, чем в 5.х
                                            Во-вторых речь то о том, что именно генератор поможет на порядок сократить потребление памяти на операциях чтения больших файлов подобного типа (текстовые файлы с большим количеством строк, логи и т.п.). Статья у нас про экономию и оптимизацию же
                                              0
                                              > Во-вторых речь то о том, что именно генератор поможет

                                              Так и я об этом же. Не пхп7 же как таковой )
                                                0
                                                Ну так времена php7 это одно утверждение.
                                                А генераторы это второе.
                                                Малосвязанные, ага
                                                0
                                                Статья у нас про экономию и оптимизацию же

                                                Статья у меня не про "как", а про "почему". Да и, если на то пошло, не про оптимизацию совсем.


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

                                                  0
                                                  экономит на памяти, но вот что касается стоимости в перфомансе

                                                  Есть расклад по опкодам и цыферки? Было бы интересно
                                                    0

                                                    По циферкам, при замене explode("\n", file_get_contents(... на генератор, на моем железе, получилось в два раза медленнее. Тут дело не столько в опкодах, сколько в том, что bulk operation, возможно за исключением некоторых случаев, которые с ходу не придумываются, всегда быстрее, чем one by one. По природе своей.


                                                    UPD Потоковая обработка хороша в случаях, когда существует фильтрация входных данных и/или условие остановки обработки.

                                    –1

                                    Сравнивал время загрузки записей из БД в массив через PDO и в объект через Доктрину. Через Доктрину в 10 раз медленнее. Использую Доктрину за исключением случаев, когда надо обработать большое число записей и получить на выходе небольшой объем информации (вроде пары чисел).


                                    Там, где нужна высокая производительность, возможно, стоит использовать не PHP.

                                      –1
                                      Ваши слова да разработчикам Magento 2 в уши. Я когда первый раз столкнулся с ней не сразу понял с чем имею дело, потом понемногу приходило понимание масштаба «абстракции».
                                      Есть предположение что в ней классов и строчек кода больше чем в каком-нибудь ядре Линукса.
                                      Без кэша загрузка страницы может занимать до 15 секунд.
                                        +2
                                        Я работал с высоконагруженными сайтами. Если стоит вопрос в производительности, надо смотреть и выпиливать ручками. Но нет единого подхода. Абстракции все равно нужны и ООП легче читать, писать, дебажить и тестировать.
                                        Из опыта, наиболее часто встречающиеся проблемы (с намеками на их решения):
                                        — зачем мы гоняем через луп миллион раз вообще?
                                        — зачем мы имеем миллион чего-то в памяти даже без лупа?
                                        — почему то что мы обрабатываем не существует еще в кэше, а снова вычисляется?
                                        Если же проблемный код не про эти вещи, то обычно абстрации не стоит обвинять.
                                        Я бы сказал, что, надо не от абстракций избавляться, а архитектуру улучшать.
                                          +3

                                          Одна из причин использования ООП не по делу в PHP — отсутствие автозагрузуи для функций. Это так, заметка.

                                            0
                                            У меня что-то в два раза разницы не получилось между первым и вторым подходом (php 7.2):

                                            hett@ubuntu:~$ php test.php
                                            Imperative: 0.041645789146423
                                            Procedural: 0.048447585105896
                                            

                                            Третий подход не стал проверять.

                                            PHP 8 (правда тут дополнительный слой виртуализации и сравнивать с перым тестом не стоит):

                                            hett@ubuntu:~$ sudo docker run -it -v "$PWD":/usr/src/app -w /usr/src/app akondas/php:8.0-cli-alpine php test.php
                                            Imperative: 0.042742681503296
                                            Procedural: 0.04987781047821
                                            


                                            Код под спойлером:
                                            Заголовок спойлера
                                            <?php
                                                $file = 'zend_vm_execute.h';
                                                $start = microtime(true);
                                                ob_start();
                                                for ($i = 0; $i < 10; $i++) {
                                                    $array = explode("\n", file_get_contents($file));
                                                    $cache = [];
                                            
                                                    foreach ($array as $row) {
                                                        if (empty($row)) continue;
                                                        $words = preg_split("/\s+/", trim($row));
                                                        if (count($words) > 10) {
                                                            $words = array_reverse($words);
                                                        }
                                                        $row = implode(" ", $words);
                                                        if (isset($cache[$row])) {
                                                            $cache[$row]++;
                                                        } else {
                                                            $cache[$row] = 1;
                                                        }
                                                    }
                                            
                                                    foreach ($cache as $key => $value) {
                                                        if ($value > 1000) {
                                                            echo "$key : $value" . PHP_EOL;
                                                        }
                                                    }
                                                }
                                                ob_end_clean();
                                                echo "Imperative: " . (microtime(true) - $start) / 10, PHP_EOL;
                                            
                                                function getContentFromFile(string $fileName): array
                                                {
                                                    return explode("\n", file_get_contents($fileName));
                                                }
                                            
                                                function reverseWordsIfNeeded(array &$input)
                                                {
                                                    if (count($input) > 10) {
                                                        $input = array_reverse($input);
                                                    }
                                                }
                                            
                                                function prepareString(string $input): string
                                                {
                                                    $words = preg_split("/\s+/", trim($input));
                                                    reverseWordsIfNeeded($words);
                                                    return implode(" ", $words);
                                                }
                                            
                                                function printIfSuitable(array $input, int $threshold)
                                                {
                                                    foreach ($input as $key => $value) {
                                                        if ($value > $threshold) {
                                                            echo "$key : $value" . PHP_EOL;
                                                        }
                                                    }
                                                }
                                            
                                                function addToCache(array &$cache, string $line)
                                                {
                                                    if (isset($cache[$line])) {
                                                        $cache[$line]++;
                                                    } else {
                                                        $cache[$line] = 1;
                                                    }
                                                }
                                            
                                                function processContent(array $input): array
                                                {
                                                    $cache = [];
                                                    foreach ($input as $row) {
                                                        if (empty($row)) continue;
                                                        addToCache($cache, prepareString($row));
                                                    }
                                                    return $cache;
                                                }
                                            
                                                $start = microtime(true);
                                                ob_start();
                                                for ($i = 0; $i < 10; $i++) {
                                                    printIfSuitable(
                                                        processContent(
                                                            getContentFromFile($file)
                                                        ),
                                                        1000
                                                    );
                                                }
                                                ob_end_clean();
                                            
                                                echo "Procedural: " . (microtime(true) - $start) / 10, PHP_EOL;
                                            
                                            
                                            
                                            


                                              –1
                                              У меня что-то в два раза разницы не получилось между первым и вторым подходом (php 7.2):

                                              Не удивительно. Железо то абсолютно разное. Сравните ваше время исполнения императивного кода 0.041 и мое 0.148. Статья же не про цифры и точные соотношения в общем то. :)

                                                +1
                                                У вас в первом случае 0.148с, во втором «0.275с… WTF!? Разница почти в 2 раза!»
                                                Смущает то, что мне не удалось получить такую большую разницу в соотношении как у Вас.
                                                По-хорошему еще бы нужно исключить чтение файла из бенчмарка. Он читается каждую итерацию.
                                                  0

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

                                                    +4
                                                    Меня смущает не скорость выполнения, а объективность проведенного вами исследования. Уж так ли дороги абстракции как сказано в статье? В моем случае оказалось, что нет.

                                                    PS^ А процессор у меня относительно старый — i7 3820, 2012 года. Еще большой вопрос какова будет разница на более свежих.
                                                      –1

                                                      А я вроде нигде не говорил, что они прям дорогие и всегда являются узким местом. Вы зачем-то пытаетесь опровергнуть то, что сами и придумали :)


                                                      Очевидно, что любые замеры всегда будут субъективны. Нельзя утверждать, что 2+2 всегда будет исполняться ровно в два раза быстрее, чем 2*2, даже если на конкретно вашем железе+ОС+окружении это и так. Но можно с довольно большой долей вероятности утверждать, что десять операций 2+2 будут выполняться дольше, чем одна.

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