Pull to refresh

Comments 52

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

это просто

file($fileName, FILE_IGNORE_NEW_LINES);

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

if (empty($row)) continue;

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

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

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

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

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

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

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

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


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

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

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

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


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

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


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

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

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

У меня что-то в два раза разницы не получилось между первым и вторым подходом (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;





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

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

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

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

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

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

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


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

Sign up to leave a comment.