
По поводу микрооптимизаций 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 года.
Да, там нет разбора на опкоды. За это, конечно, спасибо.