По поводу микрооптимизаций 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 подряд.

Комментарии (49)


  1. sainomori
    09.04.2019 16:12

    Ну вообще, вывод 1 прямо спрашивается на ZCE — это считается рекомендуемой практикой.


    1. rjhdby Автор
      09.04.2019 16:18

      Однако это довольно часто встречающееся заблуждение. Да и цель статьи в том, чтобы показать «почему это так», а не сказать «делайте так, поверьте на слово» ;)


      1. sainomori
        09.04.2019 17:05

        Не соглашусь — такая штука была серьёзно распространена во времена php4 и там далеко не всё так было просто. В всех более-менее современных гайдах уже пишут про различие.

        А бенчмарки есть на странице официальной документации в самом первом комментарии от некоего Джона от ноября 2016 года.

        Да, там нет разбора на опкоды. За это, конечно, спасибо.


  1. megahertz
    09.04.2019 18:13
    +6

    Разница больше стилистическая. Удобно по кавычкам сразу понимать — есть там подстановка или нет, особенно если строка длинная.


    1. AlexTest
      09.04.2019 19:00
      +3

      Именно так, у нас принято такое соглашение для стиля кодирования:
      двойные кавычки никогда не применяются без подстановки !


      1. rsdc127
        10.04.2019 08:15

        Поступаю аналогично, невероятно удобно читать код.


      1. 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 >


        1. AlexTest
          10.04.2019 15:08

          Так делать не стоит, для переводов строк лучше использовать свою или системную константу PHP_EOL


          1. Tangeman
            10.04.2019 15:49
            +2

            … что будет весьма неудобно если их больше одной в строке, и не имеет смысла если нужно именно \n (а не то что по дефолту в системе).


          1. vdem
            11.04.2019 21:52

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


    1. VolCh
      10.04.2019 10:29

      С другой стороны, гораздо удобнее искать строки в коде, если все кавычки одинаковы, причём независимо от языка.


      1. rjhdby Автор
        10.04.2019 10:33

        Мне кажется, что с оглядкой на подсветку синтаксиса — это несколько надуманная проблема. (собственно как и описанная в посте, на который вы отвечали)


        1. VolCh
          10.04.2019 11:21
          +1

          Я о задачах типа «вот тут сообщение об ошибке вывалилось — надо найти откуда оно вообще» или «вот тут у нас надпись — надо изменить» при слабом знакомстве с кодовой базой. А в случае PHP ещё может быть callable со строковым референсом на класс, функцию, метод. И если с классом решается использованием ::class, то с функциями методами такого нет.


          1. rjhdby Автор
            10.04.2019 16:07

            Сначала я, особо не вдумываясь, решил, что вполне себе здравый аргумент. Но что-то все равно «царапало» глаз. Начал прикидывать и вот не удалось мне придумать как единообразие кавычек спасет в данной ситуации. Не могли бы раскрыть тему, может я просто что-то не правильно понял?


            1. BoShurik
              10.04.2019 16:17
              +3

              К примеру если по коду надо найти строку function. Если просто набрать, то в выборке будет много function, которые относятся к описанию функций. Т.о. проще искать 'function, но, т.к. вариантов кавычек может быть два, надо не забыть поискать и по "function


            1. VolCh
              10.04.2019 16:39

              Выше правильно ответил BoShurik, единственно можно ещё искать по регулярке типа ['"]function В любом случае проще искать только по "function, не думая про 'function


  1. edogs
    09.04.2019 19:15

    echo 'test ',$var,'test'; имеет тот недостаток, что его по быстрому не заменишь на переменную $a='test ',$var,'test'; — придется переправлять запятую на точку.

    При прочих равных больше любим конкатенацию и одинарные, т.к. при простых переменных двойные еще норм, а вот необходимость вкрячивать фигурные в вариантах вида echo «test {$var}test» или echo «test {$a[1]}test» уже напрягает.
    Жаль что этого варианта нет в статье, как впрочем и HEREDOC и банального выхода из интерпретатора вида ?> test<?=$var?>test Статья была бы полнее.

    p.s.: Да и вообще шаблонизаторы рулят.


    1. rjhdby Автор
      09.04.2019 20:57
      +2

      Жаль что этого варианта нет в статье, как впрочем и HEREDOC и банального выхода из интерпретатора вида ?> test<?=$var?>test Статья была бы полнее.

      А там вообще никакой разницы во внутрянке — не о чем писать.


      HEREDOC и "complex string" — это ровно те же строки в двойных кавычках (ROPE).
      А выход из интерпретатора (и вход в него через <?=) — это просто отдельные операторы echo


    1. denisshabr
      10.04.2019 16:29

      php это же и есть шаблонизатор.


      1. VolCh
        10.04.2019 16:49

        Шаблонизация, предлагаемая PHP, уже давно не отвечает требованиям большинства современных веб-приложений, как минимум для ручной разработки (есть вариант кодогенерации) — прежде всего по требованиям безопасности. С другой стороны, сам язык и практики его применения давно переросли понятие "шаблонизатор", а некоторые надеются, что когда-нибудь шаблонизация в PHP будет если не полностью выпилена, то включаться принудительно в заданных разработчиком или админом случаях. Ну типа .php файлы это просто код, не требущий в начале <?php, а только какой-нибудь .phtml — php шаблоны с ограниченной функциональностью.


  1. Markus_Kane
    09.04.2019 22:12

    Спасибо за статью!
    Также было бы интересно узнать что быстрее — передать перечень аргументов одного типа, пользуясь splat оператором, или передать массив как один аргумент но с теми же значениями?


    1. rsdc127
      10.04.2019 12:18

      Массив.


      1. Markus_Kane
        10.04.2019 20:45

        А можно пруфы?)


        1. rsdc127
          10.04.2019 21:19

          eval.in/1093917

          Многое зависит от версии:
          в 5.6 передача массива работает быстрее чем список аргументов раз так в 5.
          в 7.* разница не так велика, так как в 7 оптимизировали вызов функций, об этом можно почитать в презетации Дмитрия Стогова об отимизациях в ветке 7.0.


          1. Markus_Kane
            12.04.2019 17:35

            Спасибо!


  1. johnfound
    10.04.2019 01:06
    +1

    Все описанное ниже — это, по большей части, экономия на наносекундах

    Автор использует весьма произвольно единицы измерения:


    "Веревка" — 0.0129с = 12.9мс = 12900мкс = 12900000нс
    "Конкатенация" — 0.0158 = 15.8мс = 15800мкс = 15800000нс


    Разница в 2900000нс


    А это уже экономия на миллионах наносекундах.


    1. rjhdby Автор
      10.04.2019 08:14

      Все же имелось в виду «за операцию». Разделите на 71.000 и их окажется всего 40.

      Ну и полемический прием «гипербола» никто не отменял. Кто же знал, что в полу-развлекательную статью понабегут зануды и начнут придираться к наносекундам? :)


      1. johnfound
        10.04.2019 11:00

        Гм, а мне кажется, что это замедление нельзя делить на 71000. Код в цикле наверное парситься только раз и у замедления должна быть аддитивная составляющая.


        Так что, будет ли замедление те же 40нс на строке если вращаем цикл 35500 раз и печатаем сразу 2 строки в цикле? Или вообще просто печатаем 71000 раз без цикла?


        1. rjhdby Автор
          10.04.2019 11:04
          +2

          Код в цикле наверное парситься только раз и у замедления должна быть аддитивная составляющая.

          Код парсится ровно один раз на этапе компиляции в опкоды и время это в замере не учитывается.
          Набор опкодов же не меняется на каждой итерации, так что откуда взяться аддитивности?

          Ну и в целом повторюсь — эта статья не про то, на сколько нано/микросекуд один вариант хуже другого, а про то, чем один вариант отличается от другого с точки зрения исполнения.


    1. tyomitch
      10.04.2019 10:24

      Если самое узкое место в вашем коде на PHP — это конкатенации строк, то у вас есть проблемы посерьёзнее, чем выбор между одинарными и двойными кавычками ;)


  1. Arkham
    10.04.2019 05:12

    Указывайте версию php которую разбираете. Так-то в 5-ке даже implode работал быстрее конкатенации.


    1. rjhdby Автор
      10.04.2019 16:50

      Вот умеете уговорить! :)


  1. cawakharkov
    10.04.2019 09:05

    В 99% случаев использую одинарные и конкатенации.


  1. tendium
    10.04.2019 09:46

    Обычно в банковских блогах я ожидаю увидеть информацию о Java или .NET. Поэтому стало любопытно — для каких целей в АльфаБанке используется PHP?

    P.S. Подчеркну, это НЕ для холивор, вопрос чисто из интереса.


    1. rjhdby Автор
      10.04.2019 10:01

      Да ну бросьте, какой тут может быть холивар!?
      Полуофициально используется для внутренних нужд IT, когда есть нужда упростить/автоматизировать некритичный, но занудный процесс — например формирование заявок на мониторинг.


  1. impwx
    10.04.2019 10:28

    Шел 2019 год, а phpшники все еще спорили о кавычках…


    1. rjhdby Автор
      10.04.2019 10:34
      +2

      Потому, что можем! :D
      Если есть из чего выбирать, то грех про этот выбор не поспорить.


  1. askazarinov
    10.04.2019 10:52

    Вот еще немного тестов на данную тему www.php.net/manual/ru/language.types.string.php#120160


    1. rjhdby Автор
      10.04.2019 10:56

      Удалено (это я, конечно, дал маху с переводами строки)


      1. askazarinov
        10.04.2019 10:58

        Оу… и правда… Проглядел :(


  1. evgwed
    10.04.2019 13:39

    А что по поводу двойных кавычек и sprintf?


    1. rjhdby Автор
      10.04.2019 13:55
      +1

      То же, что и по поводу printf, только echo отдельно. Алгоримт генерации конечной строки тот же самый.

      diff: user_sprintf vs user_printf
      1		- 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	  }


  1. rjhdby Автор
    10.04.2019 13:53

    . ошибся веткой


  1. NickyX3
    10.04.2019 15:25

    Конкатенация удобнее, чисто с точки зрения подсветки переменных и автоматизированного рефакторинга в IDE. Экономия на «веревках» не стоит неудобств


    1. rjhdby Автор
      10.04.2019 15:27
      +2


      1. NickyX3
        10.04.2019 15:50
        -1

        А если там метод какой? Или элемент массива? Мне вот лень в {} это все оборачивать


        1. rjhdby Автор
          10.04.2019 16:01
          +1

          Ну так здравый смысл то никто не отменял.


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


        1. mayorovp
          10.04.2019 16:01
          +1

          А конкатенировать-то, при этом, не лень?..


  1. VolCh
    10.04.2019 16:55

    foreach VS while VS for

    Если будете это тестить, то хорошо бы сравнить в соответствующих кейсах с arrary_(map|reduce|filter), в идеале с разными вариантами callable. Сколько-то лет назад быстрейшим был foreach для массивов.