По поводу микрооптимизаций PHP путем замены двойных кавычек на одинарные сломано столько копий, что внести свежую струю довольно проблематично. Но я попробую.
В данной статье будет всего один бенчмарк, куда же без него, а основной упор сделан на разбор того, как же оно устроено внутри.
Дисклаймер
- Все описанное ниже — это, по большей части, экономия на наносекундах, и на практике не даст ничего, кроме потерянного на такую микрооптимизацию времени. Особенно это касается «оптимизаций» времени компиляции.
- Я буду по-максимуму резать код и output, оставляя только самую суть.
- При написании статьи использовал PHP 7.2
Необходимые вводные
Строка в двойных кавычках на этапе компиляции обрабатывается несколько иначе, чем строка в одинарных кавычках.
Одинарные кавычки будут разбираться так:
statement
-> expr
-> scalar
-> dereferencable_scalar
-> T_CONSTANT_ENCAPSED_STRING
Двойные так:
statement
-> expr
-> scalar
-> '"' encaps_list '"'
-> Дальше строка матчится на предмет переменных внутри и, если нужно, разбивается на дополнительные токены
В статьях про микрооптимизации PHP очень часто встречается совет не использовать print, поскольку он медленнее echo. Давайте посмотрим, как они разбираются.
Разбор echo:
statement
-> T_ECHO echo_expr_list
-> echo_expr_list
-> набор echo_expr
-> expr
Разбор print:
statement
-> expr
-> T_PRINT expr
-> expr (круг замкнулся)
Т.е. в общем да, echo обнаруживается шагом раньше и этот шаг, надо заметить, довольно тяжелый.
Чтобы по ходу статьи лишний раз не акцентировать внимание, будем держать в голове, что на этапе компиляции двойные кавычки проигрывают одинарным, а print проигрывает echo. Также и не будем забывать, что речь, в худшем случае, про наносекунды.
Ну и чтобы два раза не вставать. Вот diff функций, компилирующих print и echo:
1 - void zend_compile_print(znode *result, zend_ast *ast) /* {{{ */
1 + void zend_compile_echo(zend_ast *ast) /* {{{ */
2 2 {
3 3 zend_op *opline;
4 4 zend_ast *expr_ast = ast->child[0];
5 5
6 6 znode expr_node;
7 7 zend_compile_expr(&expr_node, expr_ast);
8 8
9 9 opline = zend_emit_op(NULL, ZEND_ECHO, &expr_node, NULL);
10 - opline->extended_value = 1;
11 -
12 - result->op_type = IS_CONST;
13 - ZVAL_LONG(&result->u.constant, 1);
10 + opline->extended_value = 0;
14 11 }
Ну вы поняли — они идентичны по функционалу, но print дополнительно возвращает константу, равную 1. Думаю на этом тему с print можно закрыть и забыть о нем навсегда.
Простая строка, без изысков
Строки
echo 'Some string';
и echo "Some string";
будут разбиты практически идентично на 2(дисклаймер п2) токена. T_ECHO: echo
T_ENCAPSED_AND_WHITESPACE/T_CONSTANT_ENCAPSED_STRING: "Some string"
Причем для одинарных кавычек всегда будет T_CONSTANT_ENCAPSED_STRING, а для двойных — когда как. Если есть пробел в строке, то T_ENCAPSED_AND_WHITESPACE.
Опкоды же будут просты до безобразия и абсолютно идентичны:
line #* E I O op fetch ext return operands
-----------------------------------------------------------
4 0 E > ECHO 'Some string'
Выводы
Если хотите сэкономить пару тактов процессора на этапе компиляции, то, для константных строк, используйте одинарные кавычки.
Динамическая строка
Тут есть 4 варианта.
echo "Hello $name! Have a nice day!";
echo 'Hello '.$name.'! Have a nice day!';
echo 'Hello ', $name, '! Have a nice day!';
printf ('Hello %s! Have a nice day!', $name);
Для первого варианта:
T_ECHO: echo
T_ENCAPSED_AND_WHITESPACE: Hello
T_VARIABLE: $name
T_ENCAPSED_AND_WHITESPACE: ! Have a nice day!
Для второго (для третьего так же, только вместо точек будут запятые):
T_ECHO: echo
T_CONSTANT_ENCAPSED_STRING: 'Hello '
string: .
T_VARIABLE: $name
string: .
T_CONSTANT_ENCAPSED_STRING: '! Have a nice day!'
Для четвертого:
T_STRING: printf
T_CONSTANT_ENCAPSED_STRING: 'Hello %s! Have a nice day!'
string: ,
T_VARIABLE: $name
А вот с опкодами все будет куда как занимательнее.
Первый:
echo "Hello $name! Have a nice day!";
line #* E I O op fetch ext return operands
-----------------------------------------------------------
3 0 E > ASSIGN !0, 'Vasya'
4 1 ROPE_INIT 3 ~3 'Hello+'
2 ROPE_ADD 1 ~3 ~3, !0
3 ROPE_END 2 ~2 ~3, '%21+Have+a+nice+day%21'
4 ECHO ~2
Второй:
echo 'Hello '.$name.'! Have a nice day!';
line #* E I O op fetch ext return operands
-----------------------------------------------------------
3 0 E > ASSIGN !0, 'Vasya'
4 1 CONCAT ~2 'Hello+', !0
2 CONCAT ~3 ~2, '%21+Have+a+nice+day%21'
3 ECHO ~3
Третий:
echo 'Hello ', $name, '! Have a nice day!';
line #* E I O op fetch ext return operands
-----------------------------------------------------------
3 0 E > ASSIGN !0, 'Vasya'
4 1 ECHO 'Hello+'
2 ECHO !0
3 ECHO '%21+Have+a+nice+day%21'
Четвертый:
printf ('Hello %s! Have a nice day!', $name);
line #* E I O op fetch ext return operands
-----------------------------------------------------------
3 0 E > ASSIGN !0, 'Vasya'
4 1 INIT_FCALL 'printf'
2 SEND_VAL 'Hello+%25s%21+Have+a+nice+day%21'
3 SEND_VAR !0
4 DO_ICALL
Здравый смысл подсказывает, что вариант с `printf` будет проигрывать по скорости первым трем (тем более, что в конце там все тот же ECHO), так что оставим его для задач где нужно форматирование и больше в этой статье вспоминать не будем.
Казалось бы, третий вариант самый быстрый — напечатать последовательно три строки без конкатенаций, странных ROPE и создания дополнительных переменных. Но не все так просто. Функция печати в PHP конечно не Rocket Science, но и отнюдь не банальный Си-шный fputs. Кому интересно — клубок распутывается начиная с php_output_write в файле main/output.c.
CONCAT. Тут все просто — преобразуем, если нужно, аргументы в строки и создаем новую zend_string посредством быстрого memcpy. Единственный минус, что при длинной цепочке конкатенаций на каждую операцию будут создаваться новые строки путем перекладывания одних и тех же байтиков с места на место.
А вот с ROPE_INIT, ROPE_ADD и ROPE_END все сильно интересней. Следим за руками:
- ROPE_INIT(ext = 3, return = ~3, operands = 'Hello+')
Аллоцируем «веревку» из трех слотов(ext), помещаем в слот 0 строку 'Hello+'(operands) и возвращаем временную переменную ~3(return), содержащую «веревку». - ROPE_ADD(ext = 1, return = ~3, operands = ~3, !0)
Помещаем в слот 1(ext) «веревки» ~3(operands) строку 'Vasya', полученную из переменной !0(operands) и возвращаем «веревку» ~3(return). - ROPE_END(ext = 2, return = ~2, operands = ~3, '%21+Have+a+nice+day%21')
Помещаем в слот 2(ext) строку '%21+Have+a+nice+day%21'(operands), после чего создаем zend_string необходимого размера и копируем в нее по очереди все слоты «веревки» тем же memcpy.
Отдельно стоит заметить, что в случае констант и временных переменных в слоты будут помещаться ссылки на данные, и лишнего копирования происходить не будет.
По-моему, довольно элегантно. :)
Давайте побенчмаркаем. В качестве исходных данных возьмем файл zend_vm_execute.h (имхо это будет справедливо) на 71 тысячу строк и попечатаем его разными способами по 100 проходов, дропнув минимум и максимум (каждый замер запускал по 10 раз, выбирая наиболее часто встречающийся вариант):
<?php
$file = explode("\n", file_get_contents("C:\projects\C\php-src\Zend\zend_vm_execute.h"));
$out = [];
for ($c = 0; $c < 100; $c++) {
$start = microtime(true);
ob_start();
$i = 0;
foreach ($file as $line) {
$i++;
// echo 'line: ', $i, 'text: ', $line;
// echo 'line: ' . $i . 'text: ' . $line;
// echo "line: $i text: $line";
// printf('line: %d text: %s', $i, $line);
}
ob_end_clean();
$out[] = (microtime(true) - $start);
}
$min = min($out);
$max = max($out);
echo (array_sum($out) - $min - $max) / 98;
Что замеряем | Среднее время в секундах |
---|---|
«Веревка» | 0.0129 |
Несколько ECHO | 0.0135 |
Конкатенация | 0.0158 |
printf, для полноты картины | 0.0245 |
Выводы
- Для строк с простой подстановкой, внезапно, двойные кавычки более оптимальны, чем одинарные с конкатенацией. И чем более длинные строки используются — тем больше выигрыш.
- Аргументы через запятую… Тут много нюансов. По замеру быстрее конкатенации и медленнее «веревки», но слишком много «переменных» связанных с вводом/выводом.
Заключение
Мне сложно придумать ситуацию, когда может возникнуть потребность в такого рода микрооптимизациях. При выборе того или иного подхода более разумно руководствоваться другими принципами — например, читаемостью кода или принятым в вашей компании стилем кодирования.
Что до меня лично, то мне подход с конкатенациями не нравится из-за вырвиглазного вида, хотя в некоторых случаях он может быть оправдан.
PS Если такого рода разборы интересны — дайте знать — там много чего еще есть, далеко не всегда однозначного и очевидного: массив VS объект, foreach VS while VS for, ваш вариант… :)
Небольшое пояснение по итогам чтения комментариев
Синтаксис HEREDOC и «сложные строки»(где переменные в фигурных скобках внутри) — это те же самые строки в двойных кавычках и компилируются абсолютно аналогично.
Перемешка PHP с HTML, такого вида:
<?php $name = 'Vasya';?>Hello <?=$name?>! Have a nice day!
Это просто 3 echo подряд.
Комментарии (49)
megahertz
09.04.2019 18:13+6Разница больше стилистическая. Удобно по кавычкам сразу понимать — есть там подстановка или нет, особенно если строка длинная.
AlexTest
09.04.2019 19:00+3Именно так, у нас принято такое соглашение для стиля кодирования:
двойные кавычки никогда не применяются без подстановки !VladimirAndreev
10.04.2019 14:03а как же
user@aaa:/var/log/nginx$ php7.0 -a
Interactive mode enabled
php >
php >
php > echo '\n';
\n
php > echo "\n";
php >AlexTest
10.04.2019 15:08Так делать не стоит, для переводов строк лучше использовать свою или системную константу PHP_EOL
Tangeman
10.04.2019 15:49+2… что будет весьма неудобно если их больше одной в строке, и не имеет смысла если нужно именно \n (а не то что по дефолту в системе).
vdem
11.04.2019 21:52Всегда использую PHP_EOL, DIRECTORY_SEPARATOR и прочие константы, и тут неважно быстрее или медленнее — просто так правильнее.
VolCh
10.04.2019 10:29С другой стороны, гораздо удобнее искать строки в коде, если все кавычки одинаковы, причём независимо от языка.
rjhdby Автор
10.04.2019 10:33Мне кажется, что с оглядкой на подсветку синтаксиса — это несколько надуманная проблема. (собственно как и описанная в посте, на который вы отвечали)
VolCh
10.04.2019 11:21+1Я о задачах типа «вот тут сообщение об ошибке вывалилось — надо найти откуда оно вообще» или «вот тут у нас надпись — надо изменить» при слабом знакомстве с кодовой базой. А в случае PHP ещё может быть callable со строковым референсом на класс, функцию, метод. И если с классом решается использованием ::class, то с функциями методами такого нет.
rjhdby Автор
10.04.2019 16:07Сначала я, особо не вдумываясь, решил, что вполне себе здравый аргумент. Но что-то все равно «царапало» глаз. Начал прикидывать и вот не удалось мне придумать как единообразие кавычек спасет в данной ситуации. Не могли бы раскрыть тему, может я просто что-то не правильно понял?
BoShurik
10.04.2019 16:17+3К примеру если по коду надо найти строку
function
. Если просто набрать, то в выборке будет многоfunction
, которые относятся к описанию функций. Т.о. проще искать'function
, но, т.к. вариантов кавычек может быть два, надо не забыть поискать и по"function
edogs
09.04.2019 19:15echo 'test ',$var,'test'; имеет тот недостаток, что его по быстрому не заменишь на переменную $a='test ',$var,'test'; — придется переправлять запятую на точку.
При прочих равных больше любим конкатенацию и одинарные, т.к. при простых переменных двойные еще норм, а вот необходимость вкрячивать фигурные в вариантах вида echo «test {$var}test» или echo «test {$a[1]}test» уже напрягает.
Жаль что этого варианта нет в статье, как впрочем и HEREDOC и банального выхода из интерпретатора вида ?> test<?=$var?>test Статья была бы полнее.
p.s.: Да и вообще шаблонизаторы рулят.rjhdby Автор
09.04.2019 20:57+2Жаль что этого варианта нет в статье, как впрочем и HEREDOC и банального выхода из интерпретатора вида ?> test<?=$var?>test Статья была бы полнее.
А там вообще никакой разницы во внутрянке — не о чем писать.
HEREDOC и "complex string" — это ровно те же строки в двойных кавычках (ROPE).
А выход из интерпретатора (и вход в него через <?=) — это просто отдельные операторыecho
denisshabr
10.04.2019 16:29php это же и есть шаблонизатор.
VolCh
10.04.2019 16:49Шаблонизация, предлагаемая PHP, уже давно не отвечает требованиям большинства современных веб-приложений, как минимум для ручной разработки (есть вариант кодогенерации) — прежде всего по требованиям безопасности. С другой стороны, сам язык и практики его применения давно переросли понятие "шаблонизатор", а некоторые надеются, что когда-нибудь шаблонизация в PHP будет если не полностью выпилена, то включаться принудительно в заданных разработчиком или админом случаях. Ну типа .php файлы это просто код, не требущий в начале
<?php
, а только какой-нибудь .phtml — php шаблоны с ограниченной функциональностью.
Markus_Kane
09.04.2019 22:12Спасибо за статью!
Также было бы интересно узнать что быстрее — передать перечень аргументов одного типа, пользуясь splat оператором, или передать массив как один аргумент но с теми же значениями?rsdc127
10.04.2019 12:18Массив.
Markus_Kane
10.04.2019 20:45А можно пруфы?)
rsdc127
10.04.2019 21:19eval.in/1093917
Многое зависит от версии:
в 5.6 передача массива работает быстрее чем список аргументов раз так в 5.
в 7.* разница не так велика, так как в 7 оптимизировали вызов функций, об этом можно почитать в презетации Дмитрия Стогова об отимизациях в ветке 7.0.
johnfound
10.04.2019 01:06+1Все описанное ниже — это, по большей части, экономия на наносекундах
Автор использует весьма произвольно единицы измерения:
"Веревка" — 0.0129с = 12.9мс = 12900мкс = 12900000нс
"Конкатенация" — 0.0158 = 15.8мс = 15800мкс = 15800000нс
Разница в 2900000нс
А это уже экономия на миллионах наносекундах.
rjhdby Автор
10.04.2019 08:14Все же имелось в виду «за операцию». Разделите на 71.000 и их окажется всего 40.
Ну и полемический прием «гипербола» никто не отменял. Кто же знал, что в полу-развлекательную статью понабегут зануды и начнут придираться к наносекундам? :)johnfound
10.04.2019 11:00Гм, а мне кажется, что это замедление нельзя делить на 71000. Код в цикле наверное парситься только раз и у замедления должна быть аддитивная составляющая.
Так что, будет ли замедление те же 40нс на строке если вращаем цикл 35500 раз и печатаем сразу 2 строки в цикле? Или вообще просто печатаем 71000 раз без цикла?
rjhdby Автор
10.04.2019 11:04+2Код в цикле наверное парситься только раз и у замедления должна быть аддитивная составляющая.
Код парсится ровно один раз на этапе компиляции в опкоды и время это в замере не учитывается.
Набор опкодов же не меняется на каждой итерации, так что откуда взяться аддитивности?
Ну и в целом повторюсь — эта статья не про то, на сколько нано/микросекуд один вариант хуже другого, а про то, чем один вариант отличается от другого с точки зрения исполнения.
tyomitch
10.04.2019 10:24Если самое узкое место в вашем коде на PHP — это конкатенации строк, то у вас есть проблемы посерьёзнее, чем выбор между одинарными и двойными кавычками ;)
tendium
10.04.2019 09:46Обычно в банковских блогах я ожидаю увидеть информацию о Java или .NET. Поэтому стало любопытно — для каких целей в АльфаБанке используется PHP?
P.S. Подчеркну, это НЕ для холивор, вопрос чисто из интереса.rjhdby Автор
10.04.2019 10:01Да ну бросьте, какой тут может быть холивар!?
Полуофициально используется для внутренних нужд IT, когда есть нужда упростить/автоматизировать некритичный, но занудный процесс — например формирование заявок на мониторинг.
askazarinov
10.04.2019 10:52Вот еще немного тестов на данную тему www.php.net/manual/ru/language.types.string.php#120160
evgwed
10.04.2019 13:39А что по поводу двойных кавычек и sprintf?
rjhdby Автор
10.04.2019 13:55+1То же, что и по поводу printf, только echo отдельно. Алгоримт генерации конечной строки тот же самый.
diff: user_sprintf vs user_printf1 - PHP_FUNCTION(user_sprintf) 1 + PHP_FUNCTION(user_printf) 2 2 { 3 3 zend_string *result; 4 + size_t rlen; 4 5 zval *format, *args; 5 6 int argc; 6 7 7 8 ZEND_PARSE_PARAMETERS_START(1, -1) 8 9 Z_PARAM_ZVAL(format) 9 10 Z_PARAM_VARIADIC('*', args, argc) 10 11 ZEND_PARSE_PARAMETERS_END_EX(RETURN_FALSE); 11 12 12 13 result = php_formatted_print(format, args, argc); 13 14 if (result == NULL) { 14 15 RETURN_FALSE; 15 16 } 16 - RETVAL_STR(result); 17 + rlen = PHPWRITE(ZSTR_VAL(result), ZSTR_LEN(result)); 18 + zend_string_efree(result); 19 + RETURN_LONG(rlen); 17 20 }
VolCh
10.04.2019 16:55foreach VS while VS for
Если будете это тестить, то хорошо бы сравнить в соответствующих кейсах с arrary_(map|reduce|filter), в идеале с разными вариантами callable. Сколько-то лет назад быстрейшим был foreach для массивов.
sainomori
Ну вообще, вывод 1 прямо спрашивается на ZCE — это считается рекомендуемой практикой.
rjhdby Автор
Однако это довольно часто встречающееся заблуждение. Да и цель статьи в том, чтобы показать «почему это так», а не сказать «делайте так, поверьте на слово» ;)
sainomori
Не соглашусь — такая штука была серьёзно распространена во времена php4 и там далеко не всё так было просто. В всех более-менее современных гайдах уже пишут про различие.
А бенчмарки есть на странице официальной документации в самом первом комментарии от некоего Джона от ноября 2016 года.
Да, там нет разбора на опкоды. За это, конечно, спасибо.