Comments 52
Не любой код в будущем будет доработан. Вполне возможно, что когда понадобится код доработать — выйдет очередной новомодный фреймворк и задача будет переписать все с нуля.
Естественно, что your mileage may vary. И писать говнокод не стоит.
Все зависит от задачи. Если, допустим, у вас высоконагруженное веб-апи, то пол секунды там вполне себе оправданный выигрыш. Ну и я же не зря акцентировал внимание на "эти 500 миллисекунд, скорее всего, будут сэкономлены только небольшим участком очень горячего кода"
«Экономия» полусекунды спагетти-кодом обернётся проблемами при поддержке и расширении. Если, конечно вы не пишете код «write once and better never run».Есть еще вариант. Автогенерация/авторазворачивание кода в более простой. Упрощенно — когда 2 вариант из статьи автоматом конвертится в первый, получается нечто вроде макросов в сях. Убивается два зайца (нет говнокода и нет потерь скорости), но есть нюансы конечно.
Вот у меня был проект сети магазинов, спагети код на функциях, который поддерживался и развивался силами 1.5 разработчиков. Затем его переписали на новый модный фреймворк, с эвентами, команд басами, попытками в ДДД, естественно всё реализовали как микросервисы. На момент моего ухода, команда уже перевалила за 20 человек.
Мне как разработчику, стало легче, меньше знаешь, меньше ответственности, больше бюрократии и митингов, откровенно начал забивать и подстаиваться по скорости к остальной команде, взял себе в нагрузку стажёров и курировал проекты с ними. Благо работа была удалённая.
Как-то я оптимизировал запросы к базе данных (так как считал, что это замедляет работу тяжелого скрипта), но не получал значительного ускорения работы скрипта. Оказалось, что самое большое замедление давало именно создание объекта ActiveRecord и затем миллионы вызовов __get к виртуальным полям. Да, пришлось переписать конкретно код с объектов на массивы (с потерей возможности работы с сущностями как объектами со своими свойствами), т.е., грубо говоря, вместо $obj->getParentName() писать Something::getParentName($obj), но за счет этого получил троекратное ускорение работы алгоритма.
Итого, все эти предположения — хороши, но всегда нужно смотреть фактическое употребление памяти и процессорного времени с помощью отладчиков в конкретных алгоритмах и получать приемлемую скорость.
В качестве входного файла по традиции возьмём php-src/Zend/zend_vm_execute.h на ~70 тысяч строк.
Сделайте пожалуйста ссылкой, чтобы можно было проверить у себя.
Другой вопрос, когда весь проект написан в таком стиле — в этом случае поддержка превращается в ад. Поэтому, ИМХО, совет должен быть немного в другом — не стоит переусложнять код и вносить туда кучу абстракций перед тем, как код действительно стал хотя бы чуточку сложным. Особенно если вы пишете на PHP
Другой вопрос, когда весь проект написан в таком стиле
Как то раз один уважаемый мною писатель выложил на гитхаб движёк своего стендалона. Взыграло во мне чувство бессмысленного и беспощадного альтруизма помочь в его развитии. Форкнул, загрузил, открыл, и волосы у меня начали шевелиться в неожиданных местах — такой адище, что ни в сказке сказать, ни пером описать. Но, как говорится, взялся за гуж… В общем переписал скрипт работы с базой в лучших практиках. Не так, чтоб прям фанатично, но хотя бы по человечески. Дня два ушло только на то, чтобы разобраться, как оно вообще работает и какие граничные условия какими костылями подперты. В ответку прилетело "Это что за фигня? Классы какие-то! Нафиг, нафиг — моему скрипту уже дцать лет, я там каждый костыль знаю и мне так удобно".
Не знаю, к чему об этом рассказал. Навеяло. :)
— «А давайте нормально перепишем?..»
— «Иди в *опу,
Что там на счёт opcace, вашем тесте он включен?
Opcache имели в виду? А зачем он там? Время компиляции то нам тут не так чтоб интересно — оно практически никак не сыграет ни для одного из вариантов.
А PHP 8 с JIT уже доступен для тестов? Интересно как там дела обстоят.
explode("\n", file_get_contents($fileName));
это просто
file($fileName, FILE_IGNORE_NEW_LINES);
А тут вы отбрасываете не только пустую строку, но и строку «0»:
if (empty($row)) continue;
Не с казал бы, что это принципиально в рамках заданной темы
Генератор тут выиграет по памяти порядком
public static function readTheRealyBigFile($path) {
$handle = fopen($path, "r");
while(!feof($handle)) {
yield trim(fgets($handle));
}
fclose($handle);
}
Генераторы появились в 5.5
"Во времена" PHP 5 далеко не везде он был, а во времена PHP7 можно рассчитывать, что 5 уж точно будет :(
Во-вторых речь то о том, что именно генератор поможет на порядок сократить потребление памяти на операциях чтения больших файлов подобного типа (текстовые файлы с большим количеством строк, логи и т.п.). Статья у нас про экономию и оптимизацию же
Так и я об этом же. Не пхп7 же как таковой )
Статья у нас про экономию и оптимизацию же
Статья у меня не про "как", а про "почему". Да и, если на то пошло, не про оптимизацию совсем.
Раз зашла речь про преимущества потоковой обработки файлов, то нельзя не отметить, что хоть она и экономит на памяти, но вот что касается стоимости в перфомансе — там все довольно печально.
экономит на памяти, но вот что касается стоимости в перфомансе
Есть расклад по опкодам и цыферки? Было бы интересно
По циферкам, при замене explode("\n", file_get_contents(...
на генератор, на моем железе, получилось в два раза медленнее. Тут дело не столько в опкодах, сколько в том, что bulk operation
, возможно за исключением некоторых случаев, которые с ходу не придумываются, всегда быстрее, чем one by one
. По природе своей.
UPD Потоковая обработка хороша в случаях, когда существует фильтрация входных данных и/или условие остановки обработки.
Сравнивал время загрузки записей из БД в массив через PDO и в объект через Доктрину. Через Доктрину в 10 раз медленнее. Использую Доктрину за исключением случаев, когда надо обработать большое число записей и получить на выходе небольшой объем информации (вроде пары чисел).
Там, где нужна высокая производительность, возможно, стоит использовать не PHP.
Есть предположение что в ней классов и строчек кода больше чем в каком-нибудь ядре Линукса.
Без кэша загрузка страницы может занимать до 15 секунд.
Из опыта, наиболее часто встречающиеся проблемы (с намеками на их решения):
— зачем мы гоняем через луп миллион раз вообще?
— зачем мы имеем миллион чего-то в памяти даже без лупа?
— почему то что мы обрабатываем не существует еще в кэше, а снова вычисляется?
Если же проблемный код не про эти вещи, то обычно абстрации не стоит обвинять.
Я бы сказал, что, надо не от абстракций избавляться, а архитектуру улучшать.
Одна из причин использования ООП не по делу в PHP — отсутствие автозагрузуи для функций. Это так, заметка.
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
. Статья же не про цифры и точные соотношения в общем то. :)
Смущает то, что мне не удалось получить такую большую разницу в соотношении как у Вас.
По-хорошему еще бы нужно исключить чтение файла из бенчмарка. Он читается каждую итерацию.
Это вполне объяснимо. На каждый опкод выполняется свой собственный машинный код. Значит на вашем, более мощном/современном железе, код для конкретных опкодов выполняется более оптимально. Может использует специфические инструкции процессора, может лучше помещается в кеши, может быстрее работа с памятью, etc.
PS^ А процессор у меня относительно старый — i7 3820, 2012 года. Еще большой вопрос какова будет разница на более свежих.
А я вроде нигде не говорил, что они прям дорогие и всегда являются узким местом. Вы зачем-то пытаетесь опровергнуть то, что сами и придумали :)
Очевидно, что любые замеры всегда будут субъективны. Нельзя утверждать, что 2+2
всегда будет исполняться ровно в два раза быстрее, чем 2*2
, даже если на конкретно вашем железе+ОС+окружении это и так. Но можно с довольно большой долей вероятности утверждать, что десять операций 2+2
будут выполняться дольше, чем одна.
PHP, почём абстракции для народа?