Как стать автором
Обновить
601.81
Альфа-Банк
Лучший мобильный банк по версии Markswebb

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

Время на прочтение6 мин
Количество просмотров21K

По поводу микрооптимизаций 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 подряд.
Теги:
Хабы:
Всего голосов 97: ↑91 и ↓6+85
Комментарии52

Публикации

Информация

Сайт
digital.alfabank.ru
Дата регистрации
Дата основания
1990
Численность
свыше 10 000 человек
Местоположение
Россия