PHP. Фееричная расстановка точек над кавычками

d1tk_mf7oricg12qjka8tx0yzce.jpeg
По поводу микрооптимизаций PHP путем замены двойных кавычек на одинарные сломано столько копий, что внести свежую струю довольно проблематично. Но я попробую.

В данной статье будет всего один бенчмаркам, куда же без него, а основной упор сделан на разбор того, как же оно устроено внутри.

Дисклаймер


  1. Все описанное ниже — это, по большей части, экономия на наносекундах и на практике не даст ничего кроме потерянного на такую микрооптимизацию времени. Особенно это касается «оптимизаций» времени компиляции.
  2. Я буду по максимуму резать код и 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 все сильно интересней. Следим за руками:

  1. ROPE_INIT (ext = 3, return = ~3, operands = 'Hello+')
    Аллоцируем «веревку» из трех слотов (ext), помещаем в слот 0 строку 'Hello+'(operands) и возвращаем временную переменную ~3(return), содержащую «веревку».
  2. ROPE_ADD (ext = 1, return = ~3, operands = ~3, !0)
    Помещаем в слот 1(ext) «веревки» ~3(operands) строку 'Vasya', полученную из переменной !0(operands) и возвращаем «веревку» ~3(return).
  3. 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


Выводы


  1. Для строк с простой подстановкой, внезапно, двойные кавычки более оптимальны, чем одинарные с конкатенацией. И чем более длинные строки используются — тем больше выигрыш.
  2. Аргументы через запятую… Тут много нюансов. По замеру быстрее конкатенации и медленнее «веревки», но слишком много «переменных» связанных с вводом/выводом.


Заключение


Мне сложно придумать ситуацию когда может возникнуть потребность в такого рода микрооптимизациях. При выборе того или иного подхода более разумно руководствоваться другими принципами — например читаемостью кода или принятом в вашей компании стилем кодирования.

Что до меня лично, то мне подход с конкатенациями не нравится из-за вырвиглазного вида, хотя в некоторых случаях он может быть оправдан.

PS Если такого рода разборы интересны — дайте знать — там много чего еще есть, далеко не всегда однозначного и очевидного: массив VS объект, foreach VS while VS for, ваш вариант… :)

© Habrahabr.ru