Расширения для PHP: вот и пригодились

Движок, на котором построены все наши сайты и CRM-система, имеет свой собственный синтаксис шаблонизатора. В новой системе, Index5, мы перешли к использованию (наряду с собственным) шаблонизатора XSLT, но сейчас речь не об этом. А о том, что у меня давно чесались руки оптимизировать процесс парсинга шаблонов…

Итак, до оптимизации в движке шаблоны сливались с данными при помощи набора рекурсивных процедур, содержащих примерно такие выражения:

$result = preg_replace(«/\{([\?~:])([a-zA-Z0-9_]*)(([<>!=]{1,2})([a-zA-Z0-9_]*)){0,1}\}(.*)\{\\1\/\\2\}/iseU», «ProcessBlock(‘$0’, ‘$1’, ‘$2’, ‘$4’, ‘$5’, ‘$6’, \$vars_arr)», $result);

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

Решение, которое сразу приходит в голову практически любому веб-программисту, называется «компилирующий шаблонизатор». По этому пути идут Smarty и другие. Шаблон преобразуется в HTML-код с включением PHP-кода, которому на вход затем подаются данные… таким образом, PHP «возвращается к истокам», и довольно эффективно выполняет свое первоначальное предназначение, а именно – вставляет динамически созданное содержимое в HTML-код.

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

Я, как old-school программист, решил пойти другим путем. Вспомнив опыты 10-15-летней давности, я переписал основные компоненты нашего парсера на C++. Разумеется, без всяких регулярных выражений (в PHP их теперь тоже не осталось). Работа со строкой, как с массивом, оказывается, способна доставить эстетическое удовольствие 🙂

В общем, после этого осталось только «прикрутить» все это к PHP в виде расширения (extension). Идем в Интернет за tutorial’ами…

Всем желающим познакомиться с темой создания расширений для PHP я рекомендую две замечательных статьи: http://devzone.zend.com/node/view/id/1021 и http://devzone.zend.com/node/view/id/1022A. В них есть практически все, что понадобилось мне для создания парсера, за некоторыми исключениями, о которых расскажу ниже.

Итак, первая сложность состояла в том, как в недрах PHP представлены ассоциативные массивы. Именно из их недр мне предстояло получать данные. Главные понятия, необходимые для понимания этого процесса – HashTable и ZVAL. Работа с ними неплохо описана в приведенных выше статьях. Хуже обстоит дело с объектами/классами: по методам, которые используются для работы с ними, документации практически нет, не говоря уже о tutorial’ах. С ними мне еще предстоит разобраться, при портировании парсера для платформы Index5 – ведь там данные, сливаемые с шаблонами, хранятся не в массивах, а в объектах.

Вернемся к нашему случаю. Данные, хранящиеся в ассоциативных массивах в PHP, становятся в C++ объектами HashTable. Для работы с ними предназначен специальный набор функций, не очень удобных, надо сказать. А в нашем движке данные хранятся в многоэтажных массивах, потенциально – с неограниченной вложенностью, поэтому использовать эти функции для работы с ними становится совсем неудобно… Мое решение состоит в том, чтобы на один проход развернуть всю иерархию массивов в два обычных массива, один из которых содержит ключи, другой – значения. Ключи для многоэтажных массивов при этом собираются в одну строку. То есть, если в PHP написано:

$arr=Array(“first”=>1, “second” => Array ( Array (“number” => 2 )));

В расширении это превратится в следующую пару массивов (в условной нотации):

keys=[«first»,»second0number»];

values=[«1″,»2»];

Вторая сложность состояла в том, как вызвать функцию, содержащуюся в PHP-скрипте, из расширения. Если с вызовом функций расширения из PHP все понятно, то обратный процесс не столь очевиден. Процитирую код, позволяющий это сделать. Пусть itemname – параметр, который необходимо передать PHP-функции:

zval *function, *itemname, *retval;

zval **params[1];

MAKE_STD_ZVAL(function);

MAKE_STD_ZVAL(itemname);

ZVAL_STRING(function, «MyPHPFunction», 1);

ZVAL_STRING(itemname, «значение», 1);

params[0] = &itemname;

if(call_user_function_ex(CG(function_table), NULL, function, &retval, 1, params, 0, NULL TSRMLS_CC) == FAILURE)

zend_error(E_ERROR, «Function call failed»);

else {

if(Z_TYPE_P(retval)==IS_STRING) {

… тут мы можем обработать возвращенное значение, используя макросы Z_STRLEN_P(retval) и Z_STRVAL_P(retval)

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

ZEND_FUNCTION(MyCPPFunction) {

}

в любую другую (пусть называется innerCPPFunction), которую, предположим, вызовет эта самая MyCPPFunction. После этого компилятор начнет ругаться на оба указанных момента. Тут уже не поможет никакая документация. Идем в исходники PHP, смотреть определения этих загадочных выражений… Оказывается, function_table – это параметр, передаваемый в ZEND_FUNCTION, а за макросом TSRMLS_CC скрывается переменная tsrm_ls, тоже являющаяся одним из стандартных параметров для функций, объявленных при помощи ZEND_FUNCTION. Что ж, остается передать эти параметры в нашу innerCPPFunction:

char *result=innerCPPFunction(CG(function_table),tsrm_ls);

char *innerCPPFunction(HashTable *function_table, void ***tsrm_ls) {

}

Еще одни, третьи по счету, грабельки, на которые пришлось мне наступить, состоят в том, что глобальные переменные, объявленные в dll, сохраняют свои значения между вызовами различных функций из PHP. Так что приходится специально о них заботиться. Особенно учитывая, что схема парсинга выглядит так: сначала из PHP вызывается функция расширения, которая сливает данные с шаблоном верхнего уровня, а затем она вызывает функции из PHP, которые инициализируют содержащиеся в странице программные модули. Для каждого из них затем снова вызывается функция из расширения, которая сливает данные модуля с его шаблоном. То есть, выполнение переходит из PHP в расширение, затем обратно в PHP, затем снова в расширение. При втором вызове как раз важно не забыть о том, что глобальные переменные первым вызовом уже проинициализированы.

Ура, расширение работает! Посмотрим, что это нам дает в плане производительности.

Измеряем на старом и новом парсере, сколько времени занимает генерация не очень загруженной страницы (справочник «Клиенты» index.CRM). На загруженных результат был бы лучше, в плане выигрыша в производительности, но наша задача – взять наиболее реальную ситуацию. Получаем следующие усредненные по нескольким испытаниям значения (да, кстати, эксперимент проводится на трехъядерном Phenom’е 2.6 GHz):

 


Движок с парсером на regexp’ах, сек. Движок с парсером на extension’е, сек.
MySQL 0,0628 0,0614
Парсинг 0,0742 0,0382
Работа прикладных PHP-модулей 0,1036 0,0990
Всего 0,2480 0,1986

Небольшие отклонения во времени по блокам, не затронутым оптимизацией, спишем на неточность определения времени, и влияние посторонних процессов. Для нас главное состоит в том, что скорость парсинга выросла почти в два раза – что, как говорится, и требовалось доказать. Впрочем, внутри свежеиспеченного extension’а еще есть что оптимизировать, так что, я думаю, это не предел.

Ну, и если говорить об эмоциях – после многих-многих лет программирования исключительно на PHP было крайне приятно вернуться к родному C++. Теперь думаю, что бы еще полезного на нем написать 🙂

 

3 комментария to “Расширения для PHP: вот и пригодились”

  1. Михаил Комм:

    Серега, ты крут!
    Завидую белой завистью, я тоже чего-то давано на CPP не кодил, ( зато совсем недавно я кодил в x86 ASSEMBLER-е — позавидуй тож 🙂 )

    Вообще штука интересная, единственное что смешно — это то, что и внутри PHP бардак ))))

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

    А я щас курю Python и вполне может быть скоро прочту у вас вводный курс, для тех кому инетерсно будет конечно 😀

  2. Сергей Горшков:

    Насчет представления массивов. Внутри PHP они представлены структурой HashTable, очень громоздкой и неудобной. Для работы с ней есть специальные функции, но фактически — быстро найти в ней значение по ключу невозможно, особенно в случае многоэтажных массивов.
    Поэтому я придумал сливать ключи всех этажей в один текстовый ключ, что и проиллюстрировано примером выше. Нолики — это числовые индексы. То есть, на первом этаже массива у меня индекс текстовый, на втором — числовой, на третьем — снова текстовый. При их сращивании получается текстовый индекс типа second0number. Такое хранение индексов очень удобно при реализации рекурсивной функции сращивания шаблонов и данных. Шаблон ведь тоже многоэтажный. Когда в шаблоне встречается тег, обозначающий цикл (в нашем синтаксисе {:cycle}), функция, разбирающая шаблон, берет кусок кода внутри {:cycle}…{:/cycle}, формирует префикс для ключа (равен «cycle0», для первого вызова в данном случае), и передает их как параметры самой себе — рекурсивно, и столько раз, сколько элементов в массиве cycle (т.е. если в массиве cycle было два элемента, первый раз она вызовет себя с шаблоном и префиксом «cycle0», второй раз — с тем же шаблоном и префиксом «cycle1»). Соответственно, очередной экземпляр этой функции, который парсит кусок шаблона внутри {:cycle}…{:/cycle}, ко всем встречающимся внутри переменным будет добавлять впереди полученный в виде параметра префикс.
    Пример.
    Пусть шаблон такой: …{:cycle}…{!value}…{:/cycle}…
    Массив, ему соответствующий:
    Array(«cycle»=>Array(Array(«value»=>»first»),Array(«value»=>»second»)));
    В CPP он развернется в такой:
    cycle0value=>first
    cycle1value=>second

    Функция парсера (назовем ее условно parse) имеет следующую структуру:

    char *parse(char *tpl,char *prefix) {

    for(перебираем элементы встретившегося цикла) {
    newprefix=strcat(prefix,»имя цикла»);
    strcat(newprefix,»номер итерации в цикле»);
    parse(tpl,newprefix);
    }

    }

    А про Python — обязательно тебя послушаем! 🙂

  3. vkontakte.ru Елена Хохолева:

    Первым делом захотелось написать «много букв — ни асилил». 😉 Но это не соответствует действительности, Сергей как раз всё понятно написал. Респект. 🙂

    А по Питону у меня уже лет 10 лежит неоткрытым толстый учебник, который мне подарил автор этого учебника в надежде, что хоть кто-то еще будет писать на Python. Так что тоже бы не против послушать. А то уже даже перед самой собой стыдно за то, насколько php и сопутствующие неприятности вытесняют из моей жизни другие языки.

Коментарии