PHP. Фееричная расстановка точек над кавычками
По поводу микрооптимизаций PHP путем замены двойных кавычек на одинарные сломано столько копий, что внести свежую струю довольно проблематично. Но я попробую.
В данной статье будет всего один бенчмаркам, куда же без него, а основной упор сделан на разбор того, как же оно устроено внутри.
Дисклаймер
- Все описанное ниже — это, по большей части, экономия на наносекундах и на практике не даст ничего кроме потерянного на такую микрооптимизацию времени. Особенно это касается «оптимизаций» времени компиляции.
- Я буду по максимуму резать код и output, оставляя только самую суть.
Необходимые вводные
Строка в двойных кавычках на этапе компиляции обрабатывается несколько иначе, чем строка в одинарных кавычках.
Одинарные кавычки будут разбираться так:
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 раз выбирая наиболее часто встречающийся вариант):
Что замеряем | Среднее время в секундах |
---|---|
«Веревка» | 0.0129 |
Несколько ECHO | 0.0135 |
Конкатенация | 0.0158 |
printf, для полноты картины | 0.0245 |
Выводы
- Для строк с простой подстановкой, внезапно, двойные кавычки более оптимальны, чем одинарные с конкатенацией. И чем более длинные строки используются — тем больше выигрыш.
- Аргументы через запятую… Тут много нюансов. По замеру быстрее конкатенации и медленнее «веревки», но слишком много «переменных» связанных с вводом/выводом.
Заключение
Мне сложно придумать ситуацию когда может возникнуть потребность в такого рода микрооптимизациях. При выборе того или иного подхода более разумно руководствоваться другими принципами — например читаемостью кода или принятом в вашей компании стилем кодирования.
Что до меня лично, то мне подход с конкатенациями не нравится из-за вырвиглазного вида, хотя в некоторых случаях он может быть оправдан.
PS Если такого рода разборы интересны — дайте знать — там много чего еще есть, далеко не всегда однозначного и очевидного: массив VS объект, foreach VS while VS for, ваш вариант… :)