9 April 2019

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

Альфа-Банк corporate blogAbnormal programmingPHPProgramming

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

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

Дисклаймер


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

  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 раз, выбирая наиболее часто встречающийся вариант):

<?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

Выводы


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

Заключение


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

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

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

Небольшое пояснение по итогам чтения комментариев


Синтаксис HEREDOC и «сложные строки»(где переменные в фигурных скобках внутри) — это те же самые строки в двойных кавычках и компилируются абсолютно аналогично.

Перемешка PHP с HTML, такого вида:
<?php $name = 'Vasya';?>Hello <?=$name?>! Have a nice day!

Это просто 3 echo подряд.
Tags:phpphp internalsбитва кавычекоптимизация
Hubs: Альфа-Банк corporate blog Abnormal programming PHP Programming
+85
19k 61
Comments 52
Top of the last 24 hours