Строки в PHP

    В последнее время обсуждения языка PHP на хабре сводятся больше к возможностям проектировать сложные системы, что не может не радовать. Однако, просмотрев с десяток самых признаваемых веб-фреймворков (Zend Framework, Adept, CakePHP, CodeIgniter, LIMB, Symfony, MZZ и другие) я с искренним удивлением обнаружил в некоторых существенные недочеты с точки зрения элементарной оптимизации.

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



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

    Итак, поехали… Задача предельно проста: провести эксперименты по скорости формирования строк из подстрок в одинарных и двойных кавычках. В принципе, этот вопрос будет актуален еще долгое время в связи с особенностями обработки строк в PHP.

    Существует немало статей по базовой оптимизации скриптов как на русском, так и на других языках. Про строки там говорится мало, но отмечается факт «парсинга» строк в двойных кавычках на предмет переменных и управляющих символов (врочем, как и в официальной документации). На основе этого логично предположить, что использование строк в двойных кавычках в работе будет несколько медленнее, чем те же операции с подстроками в одинарных кавычках.

    Кроме подстановки переменных в строки и конкатенации переменных с подстроками, в PHP реализован еще как минимум один способ формирования строк: работа с функцией sprintf. Логично предположить, что данный метод будет существенно уступать «стандартным» из-за лишнего вызова функции и парсинга строки внутри.

    Единственное дополнение, перед тем, как я представлю вам код тестового скрипта: необходимо учитывать 2 возможных варианта работы со строками в двойных кавычках: с учетом простого и «продвинутого» стиля кодирования. На то, что переменные стоят в самом начале строк обращать внимания не стоит, наверное — они являются только примерами:
     $string = "$_SERVER['HTTP_HOST'] — не администрация Ульяновской области. Мы любим русский язык и не любим тех, кто его ..."

    и
     $string = "{$_SERVER['HTTP_HOST']} — не администрация Ульяновской области. Мы любим русский язык и не любим тех, кто его ..."


    Тест номер один.
    Ну, вроде бы, все оговорки сделаны — пора показывать результаты работы. Исходный код тестировщика можно найти здесь.

    Скриншоты профайлера (копии) располагаются тут, тут и тут.

    По скриншотам видно, что моя гипотеза не подтвердилась. Единственно-верным оказалось предположение о работе со строками через sprintf. Наиболее быстрыми оказались функции, работающие именно с двойными кавычками.

    После непродолжительного обдумывания ситуации объяснение пришло само собой: все дело в том, что эталонная строка, в которую производились подстановки является слишком короткой: проход парсера по такой строке — дело плевое. Однако, даже тут видно, что нативная подстановка переменной в строку дает преимущество перед «продвинутым стилем».
    В этом же и слабость подхода конкатенации: объемы вставляемых данных превышают объемы подстрок. Откуда берутся накладные расходы можно прочесть в уже упомянутом хабратопике.

    Однако, даже эти мысли нужно было подтвердить. Для этого понадобился второй тест с изменениями на возможные упомянутые причины столь непредсказуемого (для меня) поведения. Видимо уж очень многое подкрутили в пятой версии (признаюсь, в пятой версии php я проводил только 1 тест: на обход элементов массивов).

    Тест номер два.
    Вторая гипотеза: удлинение эталонной строки даст, в конечном счете, увеличение процентного отношения времени работы функций-тестировщиков, связанных с формированием строк в двойных кавычках, относительно результатов теста номер 1. Такая же ситуация, теоретически, должна наблюдаться и в отношении работы функции sprintf. Связано это, в первую очередь, с необходимостью парсинга строк и увеличением времени, затраченных на это. В ситуации с конкатенацией подстрок в одинарных кавычках, думаю, будет наблюдаться примерно тот же результат, что и в первом тесте, что даст небольшое уменьшение доли времени выполнения функции quotes_3() ко времени работы всего скрипта (но никак не прирост производительности).

    Исходники скрипта лежат здесь,
    Копии скриншота можно найти тут, тут и тут.

    Выводы, собственно только положительные и подтверждающие гипотезу. При незначительном увеличении эталонной строки появляется большая нагрузка, что приводит к падению быстродействия функций работы с двойными кавычками и sprintf.

    Предположение относительно строк в одинарных кавычках так же оказалось верным: вместо 36,75% времени в первом тесте, во втором функция quotes_3() заняла 33,76% времени исполнения скрипта

    Практическая ценность.
    Говоря простым языком, абстрагируясь от данных, можно сделать вывод: чем длиннее строка, в которой необходимо произвести подстановку, тем больше вероятность, что операция-конкатенация выполнится быстрее, чем поиск переменной в двойных кавычках. Добровольцы могут попробовать подобрать необходимые параметры вставки (количество переменных, длина эталонной строки, длины строк в переменных) такими, чтобы они удовлетворяли равенству времен выполнения.

    Вот, собственно, и все. Остается только добавить, что в программировании мелочей не бывает (это я любителям сказать «экономия на спичках» (с) Adelf ). Зная такие тонкости и принимая их во внимание, можно писать код, который будет оптимизирован на всех своих уровнях ;)

    PS:
    Тесты проведены с помощью Zend Studio For Eclipse 6.0.0 (Debugger + Profiler included).
    Версия PHP 5.2.5
    ОС Debian Linux

    PPS:
    Буду рад, если кто-то выложит свои результаты данных тестов. Думаю, это позволит более объективно составить оценку необходимости использования того либо иного метода подстановки в строки. Так же буду признателен здоровой критике стиля изложения и оформления.

    Всем спасибо за внимание :)
    Share post

    Comments 29

      +7
      wayly, я внимательно прочитал вашу статью, но честно говоря ничего не понял. Много текста, но о чем он — осталось загадкой. Текст может и полезен, но очень тяжело написан.

      Зачем три версии одних и тех же скриншотов в разных местах? Досточно одной.

      Более того, то о чем вы пишете — это экономия на спичках, и не стоит такоих сложных запутанных экспериметнтов. Но тем неменее.

      Вывод :-) Тема не раскрыта, но за старания спасибо. Топику минус, в карму плюс. :-)

      С наилучшими.
        +5
        > Остается только добавить, что в программировании мелочей не бывает

        Использование поговорки на умиляет голословности утверждения.
        Бывает, бывает! Еще ох как бывает!
          –16
          Хех. Голословность? Цифры есть голословность? Вы меня удивили — честно.

          Уверяю вас, я не собираюсь оставлять затеи с освещением возможной оптимизации кода на простейшем уровне. :)

          Своей очереди ждут тесты схем обработки массивов, тесты производительности 2,3,4-мерных массивов, итераторов, столь популярных в последнее время геттеров/сеттеров (и прочих magic-вещей)…

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

          Только вот, в большинстве своем, наиболее шустрые и легкие техники являются стандартными, не допускающими либеральности. Это ли не есть чувство вкуса и красота кода? :)
            0
            Главное не забудьте потом вспомнить, что >80% времени, которое тратится обычным веб-скриптом, идёт на получение данных из БД. А писать на PHP скрипты-парсеры, работающие со строками не стоит, большую оптимизацию даст переход на другой язык.
              0
              Послушайте следующую мысль: она будет полезна.
              1. Я не говорил ни о каком другом языке: в топике рассмотрен только PHP и только аспект «строка в PHP». Так что говорить о других языках — это как «Я тебе про Фому, ты мне про Ерему».
              2. Получение (не обработку данных) в PHP из БД оптимизировать нельзя — это нативный функционал. Лезьте в сошники и правьте — тогда поговорим. :)

              Не бывает оптимизированных скриптов, который оптимизированы только в одну сторону. Оптимизация ПО — процесс, затрагивающий каждый файл, каждую строку кода :)
                0
                Получение (не обработку данных) в PHP из БД оптимизировать нельзя — это нативный функционал.
                Грамотно спроектированная структура данных и нормальная работа с базой (например, не надо вызывать 'select * from table' в цикле) дадут куда большую оптимизацию, чем расстановка кавычек и разные способы конкатенации строк.

                Не бывает оптимизированных скриптов, который оптимизированы только в одну сторону. Бывает, когда за деревьями не видно леса и всесторонняя оптимизация становится латанием дыр треснувшего пополам корабля.
            • UFO just landed and posted this here
            0
            Ок. Отклонимся от статистики (таки математика элементарная — не всем ясна до конца).

            Вся фишка в том, что работа со строками, изначально есть слабое место в PHP — так уж сложилось. В большинстве фреймворков используется генерация запросов к СУБД (в основном, конечно же, MySQL) средствами подстановки в строки значений.

            Пример — $q = «SELECT „.$fields.“ FROM {$table} WHERE $conditions». Он является реальным, а не надуманным: использование трех методов генерации строки является, само по себе, противоречащим здравому смыслу.

            Еще более веселым является использование генератора запросов а-ля
            $result = DB:: query(«SELECT „.$fields.“ FROM %s WHERE $conditions», $table); Я, почему-то уверен, что 50% разработчиков приложений (и доработчиков, в том числе) встречались с такими конструкциями.

            Поскольку, создание запросов, само по себе не является одиночной операцией (мне знакомы системы, которые создают по 50 запросов на вывод страницы) и не кешируется, в большинстве своем, то из спичек, собранных на таких мелочах можно построить нечто большее :)

            Дело даже не в выигрыше во времени (он действительно будет плюшевым — 5 сотых секунды не играют роли при несущественных нагрузках), но больше в прививании подрастающему поколению «хорошего» стиля кодирования, подразумевающего некий стандарт, который, к тому же, является еще и более верным и логичным ;)

            Offtop: три картинки, ибо хостеры изображений часто сносят оные по причине необращения. «На всякий случай».
              +3
              > он действительно будет плюшевым — 5 сотых секунды не играют роли при несущественных нагрузках
              5 сотых? 5 десятитысячных уж. И то в самом худшем случае.

              >мне знакомы системы, которые создают по 50 запросов на вывод страницы
              Вы познакомились с плохими системами. У вас время, которые выполняются 50 запрсов по сравнению с генерацией строк — это соотношение веса мухи и слона. Без малейшего преувеличения.

              > $result = DB:: query(«SELECT „.$fields.“ FROM %s WHERE $conditions», $table);
              Вот в этом как раз есть смысл. :-) Не так как в вашем примере, но использование такого вполне оправдано — и даже нужно. Как думайте, почему? :-)

              > Offtop: три картинки, ибо хостеры изображений часто сносят
              Ну так вы напишите, что что это копии. Непонятно же… :)

                +1
                > Ну так вы напишите, что что это копии. Непонятно же… :)
                1. Скриншоты профайлера (копии) располагаются…
                2. Копии скриншота можно найти тут, тут и тут.

                :)

                По таблице видно, что 50 запросов — это тысячная секунды — тут я немного потерялся с нулями, аха — согласен.

                На тему «Вот в этом как раз есть смысл. :-)», отвечу так: есть смысл в конструкциях типа
                $query = sprintf(«SELECT %s FROM `%s` WHERE %s», '`name`,`surname`', DB_PREFIX.self::$db_table, «`id_user`=$id»);

                В том же limb есть такое (первое, что встретилось)
                $sql = «SELECT {$table}.* FROM {$table}, {$join_table}
                WHERE {$table}.». $object->getPrimaryKeyName(). «={$join_table}.$foreign_field AND
                {$join_table}.{$field}=». $this->owner->getId(). ' %where%';

                Кто-нибудь может объяснить, на кой фиг смешивать 3 стиля подстановки значений в строку? Всему виной либерализм PHP.

                Да, игры с временем — весьма относительный (относительно наилучшего метода). Я знаю многих программистов, которые оперируют далеко не базовыми методами оптимизации. Тем не менее, перед релизом подобные вещи подвергаются рефакторингу :)

                Не думаю, что они таким образом экономят время. Скорее, делают скрипты более стандартизированными, чтоли ;)
                  0
                  в конструкциях вида «выбрать из где, имя-фамилия, таблица, 1234» смысла нет :-)
                  вся прелесть sql в том, что он похож на естественный язык и также естественно воспринимается. хотя, конечно, если Йода учитель ваш был…
                    0
                    $q = sprintf('SELECT %s FROM table WHERE id=%d', '`field`', $_GET['id']). $_GET['id'] можно и не проверять даже :)

                      0
                      $q= build_sql(' select field from table where id = ',$_GET['id'],' and 1 = 1 ')
                      тут тоже не надо ничего проверять, но все значения на своих местах.
                        0
                        А вот тут таки надо проверять. На SQL-инъекцию, хотябы ;)
                          0
                          не надо. упомянутая конструкция экивалентна:
                          $q= ' select field from table where id = '. mysql_real_escape_string( $_GET['id'] ). ' and 1 = 1 ';

                          функция экранирования, разумеется, выбирается в соответствии с подключённой субд…
                            0
                            Эм… данные автоматически эскейпятся? Ссылку на ман можно?
                              0
                              мана нет, но есть сорц: d-o-b.ru/?source:db.driver.sqlite

                              serialize — это и есть тот самый build_sql
                                0
                                Ааа, ну все понятно. Извиняюсь — «привидился» родной mysql_query
                +1
                Ну знаете, можно долго кавычки заменять на одинарные, чтобы конкантенация была быстрее, но какой-нибудь тупейший запрос к бд или не поставленный/поставленный индекс, либо цикл в цикле сведет это все на нет.
                И «генератор запросов» не просто так же используют от нечего делать…
                А еще, как мне кажется, решающее значение имеет длинна строки.
                  +1
                  Видимо, никто не читает полностью (не осилили? :)).
                  Я трижды сказал, что вопрос не только в быстродействии, но и восприятии кода

                  > Ну знаете, можно долго кавычки заменять на одинарные, чтобы конкантенация была быстрее,
                  Видимо, вы плохо прочли статью либо не осилили ее. Вероятность того, что конкатенация будет быстрее возрастает только с увеличением строки, в которую необходимо подставить переменные. Это же очевидно по двум тестам.
                    0
                    мне вот так сложнее воспринимать:
                    mysql_query('SELECT * FROM table WHERE id=\''.$_SESSION['id_user'].'\' ');
                    нежели так:
                    mysql_query(«SELECT * FROM table WHERE id='{$_SESSION['id_user']}' „);

                    а вам?
                      0
                      mysql_query('select * from table where id=«'. $_SESSION['id']. '»'); попробуйте — будет немного легче. Эскейпинг всега вредит восприятию.
                        0
                        mysql_query(«SELECT * FROM table WHERE id={$_SESSION['id_user']}»);
                        Внесите эту строку в нормальную IDE с подсветкой или на тот же dumpz — уверяю, будет выглядеть иначе, чем здесь (или на тот же dumpz).
                  –18
                  А мне понравилась статья. Просто, но интересно.
                  В условиях работы с высоконагруженными серверами экономия на спичках при каждой итерации позволит сэкономить кубометры дров.
                    +2
                    Не хватает раздела «Итоги-Выводы» в конце статьи, с приведением таблицы результатов и т.п.
                    Наглядности тобишь конечной не хватает))
                      +1
                      я думаю, что стоит ещё и heredoc протестировать.
                        0
                        Повидимому следующей статьей автора будет: «что выбрать для выводы echo или print».

                        Спасибо за старания, но это действительно оптимизация на спичках, хотя я и повторяюсь. Но как правило, я не видел еще ни одного сайта у которого были проблемы с быстродействим, и заменой print на echo, и вставку переменных в двойные кавычки на конкатенацию — эти проблемы решились бы. Как правило, есть гораздо более узкие места, в том числе как Вы и упомянули — архитектурные решения.
                          0
                          те кому важна экономия на спичках не будут оптимизировать используя какие то одинарные кавычки для строк, а будут писать расширения на С для таких специализированых задач, тот же sql генератор например.
                            0
                            В чем автор прав — так это в том, что на Хабре наблюдается серьезный перекос в сторону фреймворков и вообще серьезного php-программирования. Это полезно и нужно, но немного обидно, что статей о технических деталях самого языка маловато.

                            А вот строки и их оптимизацию я бы вообще не рассматривал в контексте оптимизации кода и ускорения программы. Я бы скорее отнес это к стилям кодирования. Я думаю, что каждый из нас выбирает себе некоторый стиль работы со строками в зависимости от удобства написания программы и удобства чтения кода.

                            Мне например, удобнее использовать строки в одинарных кавычках с конкатенацией с переменными потому что: а.) сразу видны переменные в любом редакторе, б.) это пусть и совсем примитивное, «низкоуровневое», но все-таки разделение логики, которое дисциплинирует пишущего и упрощает понимание программы.

                            Only users with full accounts can post comments. Log in, please.