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


    По поводу микрооптимизаций 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 подряд.
    Альфа-Банк
    161,00
    Компания
    Поделиться публикацией

    Комментарии 52

      0
      Ну вообще, вывод 1 прямо спрашивается на ZCE — это считается рекомендуемой практикой.
        +7
        Однако это довольно часто встречающееся заблуждение. Да и цель статьи в том, чтобы показать «почему это так», а не сказать «делайте так, поверьте на слово» ;)
          0
          Не соглашусь — такая штука была серьёзно распространена во времена php4 и там далеко не всё так было просто. В всех более-менее современных гайдах уже пишут про различие.

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

          Да, там нет разбора на опкоды. За это, конечно, спасибо.
        +13
        Разница больше стилистическая. Удобно по кавычкам сразу понимать — есть там подстановка или нет, особенно если строка длинная.
          +9
          Именно так, у нас принято такое соглашение для стиля кодирования:
          двойные кавычки никогда не применяются без подстановки !
            0
            Поступаю аналогично, невероятно удобно читать код.
              +1
              а как же
              user@aaa:/var/log/nginx$ php7.0 -a
              Interactive mode enabled

              php >
              php >
              php > echo '\n';
              \n
              php > echo "\n";

              php >
                0
                Так делать не стоит, для переносов строк лучше использовать свою или системную константу PHP_EOL
                  +4
                  … что будет весьма неудобно если их больше одной в строке, и не имеет смысла если нужно именно \n (а не то что по дефолту в системе).
                    0
                    Всегда использую PHP_EOL, DIRECTORY_SEPARATOR и прочие константы, и тут неважно быстрее или медленнее — просто так правильнее.
                0
                С другой стороны, гораздо удобнее искать строки в коде, если все кавычки одинаковы, причём независимо от языка.
                  0
                  Мне кажется, что с оглядкой на подсветку синтаксиса — это несколько надуманная проблема. (собственно как и описанная в посте, на который вы отвечали)
                    +1
                    Я о задачах типа «вот тут сообщение об ошибке вывалилось — надо найти откуда оно вообще» или «вот тут у нас надпись — надо изменить» при слабом знакомстве с кодовой базой. А в случае PHP ещё может быть callable со строковым референсом на класс, функцию, метод. И если с классом решается использованием ::class, то с функциями методами такого нет.
                      0
                      Сначала я, особо не вдумываясь, решил, что вполне себе здравый аргумент. Но что-то все равно «царапало» глаз. Начал прикидывать и вот не удалось мне придумать как единообразие кавычек спасет в данной ситуации. Не могли бы раскрыть тему, может я просто что-то не правильно понял?
                        +4

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

                          +2

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

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

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

                  p.s.: Да и вообще шаблонизаторы рулят.
                    +3
                    Жаль что этого варианта нет в статье, как впрочем и HEREDOC и банального выхода из интерпретатора вида ?> test<?=$var?>test Статья была бы полнее.

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


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

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

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

                      0
                      Спасибо за статью!
                      Также было бы интересно узнать что быстрее — передать перечень аргументов одного типа, пользуясь splat оператором, или передать массив как один аргумент но с теми же значениями?
                        0
                        Массив.
                          0
                          А можно пруфы?)
                            0
                            eval.in/1093917

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

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


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


                      Разница в 2900000нс


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

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

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

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


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

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

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

                            Ну и в целом повторюсь — эта статья не про то, на сколько нано/микросекуд один вариант хуже другого, а про то, чем один вариант отличается от другого с точки зрения исполнения.
                          0
                          Если самое узкое место в вашем коде на PHP — это конкатенации строк, то у вас есть проблемы посерьёзнее, чем выбор между одинарными и двойными кавычками ;)
                          +2
                          Указывайте версию php которую разбираете. Так-то в 5-ке даже implode работал быстрее конкатенации.
                            0
                            Вот умеете уговорить! :)
                            +1

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

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

                              P.S. Подчеркну, это НЕ для холивор, вопрос чисто из интереса.
                                +3
                                Да ну бросьте, какой тут может быть холивар!?
                                Полуофициально используется для внутренних нужд IT, когда есть нужда упростить/автоматизировать некритичный, но занудный процесс — например формирование заявок на мониторинг.
                                –8
                                Шел 2019 год, а phpшники все еще спорили о кавычках…
                                  +5
                                  Потому, что можем! :D
                                  Если есть из чего выбирать, то грех про этот выбор не поспорить.
                                  0
                                  Вот еще немного тестов на данную тему www.php.net/manual/ru/language.types.string.php#120160
                                    –1
                                    Удалено (это я, конечно, дал маху с переводами строки)
                                      0
                                      Оу… и правда… Проглядел :(
                                    0
                                    А что по поводу двойных кавычек и sprintf?
                                      +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	  }

                                      0
                                      . ошибся веткой
                                        0
                                        Конкатенация удобнее, чисто с точки зрения подсветки переменных и автоматизированного рефакторинга в IDE. Экономия на «веревках» не стоит неудобств
                                          +2
                                            –3
                                            А если там метод какой? Или элемент массива? Мне вот лень в {} это все оборачивать
                                              +2

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


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

                                                +2
                                                А конкатенировать-то, при этом, не лень?..
                                            0
                                            foreach VS while VS for

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

                                              0
                                              Логично, что foreach быстрее. В arrary_(map|reduce|filter) вызывается функция на каждую итерацию.
                                                0
                                                Ну, оптимизацию, которая инлайнит тело функции легко представить, особенно если функция анонимная, то есть используется только здесь.
                                                  0

                                                  В случае функций стандартной библиотеки инлайнинг не пройдет. Так как они по сути своей интринсики, то это будет быстрый перебор хэш-таблицы внутри Си-функции (ZEND_HASH_FOREACH_KEY_VAL_IND и т.д.) с вызовом на каждой итерации закешированного массива опкодов (zend_call_function(&fci, &fci_cache))


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

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