Почему мотоцикл не смог заменить танк, или перевод сайта REG.RU с Template::Toolkit на Text::Xslate

в 13:24, , рубрики: reg.ru, Блог компании REG.RU, скорость работы, шаблонизаторы, метки: , ,

За любым крупным интернет-проектом стоит автоматизированная информационная система и сайт, продающий товары или услуги. Чем крупнее проект, тем сложнее логика сайта, и тем большую нагрузку ему приходится нести. Возникают задачи увеличения «мощности» сайта и уменьшения времени отклика страниц. Как и все, кто пишет подобные системы, периодически мы проводим сессии по тюнингу скорости работы нашего сайта. Оптимизируем всё, до чего можем дотянуться. На определённом этапе упёрлись в скорость работы HTML-шаблонизатора, который сходу не совсем понятно как «разогнать». Кое-что удалось выжать с помощью кэширования подшаблонов, но, несмотря на полученные позитивные результаты, время работы шаблонизатора всё равно оставалось краеугольным камнем в скорости генерации страниц. Нужны были более радикальные меры, возможно даже другие шаблонизаторы…

Об истории одной из наших инициатив в нелёгком деле поиска Святого Грааля самого быстрого шаблонизатора читайте ниже в подробном отчете Дмитрия Карасика, который был привлечён к решению этой задачи:

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

Регистратор REG.RU столкнулся с необходимостью увеличить скорость работы своего сайта, написанного на Template::Toolkit, и поручил мне решить эту задачу. Меня приятно удивило то, что люди внутри компании, проанализировав возможные решения, решили перевести проект на другой шаблонизатор, Text::Xslate, сохранив при этом полный синтаксис Template::Toolkit (в дальнейшем TT).

Я был наслышан об Xslate, но ранее дела с ним не имел. По заявленным на веб-сайте рекламным данным он обгоняет ТТ в 158 раз, и если «хоть одна половина из того, что он говорил, правда» (с), то это выглядит отличным вариантом. Да и сомневаться особо было не из-за чего – большая часть проекта написана на С, а он уж, как минимум, сам по себе быстрее перла. Более того, Xslate частично поддерживает синтаксис ТТ. Посмотрев в код, мне показалось, что все «оk» – полной поддержки нет, наверное, оттого, что автор сконцентрировался именно на шаблонизаторе, оставив базовые наметки тем, кому будет интересно расширять Xslate в этом направлении (своего рода приглашение поучаствовать в проекте). Выглядело все это вполне многообещающе, и я решил допилить Xslate до того состояния, чтобы он поддерживал TT в полном объеме или в достаточно полном для заказчика, о чем мы с REG.RU радостно договорились. В выигрыше будут, как казалось, все – клиент получит убыстренный в 158 раз код, Xslate получит доработанный синтаксис и новых пользователей, комьюнити получит отличную альтернативу TT2, а я, соответственно, профит и перо в шляпу.

Проблемы начались практически сразу. Автор Xslate Goro Fuji (увы-увы) не ответил ни на письма, ни на IRC. Мое предложение договориться, как расширить модуль, чтобы он в наибольшей мере соответствовал авторскому замыслу, фактически провалилось. Может, мое письмо было составлено не в соответствии с правилами японского этикета. Мне доводилось сталкиваться на конференциях с программистами из Японии, и, если вживую мне не удавалось наладить контакт глубже, чем дежурные улыбки и «привет-привет», то уж в онлайне, наверное, все еще сложнее.

«Ну, это ничего», – подумал я, – «В конце концов, мантра RTFS еще никогда не подводила». Если код качественный, всегда можно опубликовать патч или форкнутый проект, даже если автор не желает сотрудничать. В конце концов, я не претендую на понимание того, как развивать Xslate в качестве проекта, а только собираюсь использовать его высокооптимизированный движок в качестве базы.

И я начал изучать модуль. Справедливости ради, стоит сказать, что и код, и архитектура Xslate великолепны. Их стоит изучать студентам, да и программистам с опытом тоже не мешало бы глянуть. Xslate построен вокруг того факта, что шаблонизаторы, в основном, гоняют куски текста взад-вперед, и оптимизация начиналась уже на этом уровне – замены обычной перловой конкатенации

$a .= $b

на переписанный на C такой же аналог, но максимально облегченный. Еще, весь процессинг тоже вынесен в C: атомарные операции шаблонизатора (например, указанная выше конкатенация или операции в терминах ТТ как [% IF %], [% CALL %] и т.д.) не только написаны на C, но и сам цикл прогона файла шаблона написан на нем. Это значит, что были сэкономлены такты, требующиеся на вызов процедур (хоть бы и написанных на С) из перла – а это достаточно дорогая операция. Этот трюк оказался возможным, потому что шаблон, который выглядит достаточно обычно, как, например, написанный на ТТ

[%IF a %]
text a
[% ELSE %]
text b
[% END %]

Xslate транслирует в псевдо-ассемблерные команды вида

.if a
.literal «text a»
.print #1
.goto 3
.literal «text b»
.print #2
.end

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

Template::Document->new(
BLOCK => sub {

eval {

if ($a) {
$output .= «text a»;
} else {
$output .= «text b»;
}

}
}
)

а затем подгружается обычным eval.
Но и этого автору Xslate показалось мало! Код, выполняющий индивидуальные «ассемблерные» команды, был сделан не так, как можно было бы подумать, например (схематично):

for (i = 0; i < opcodes.length; i++) {
opcodes.list[i].callback();
}

а используя т.н. шитый код:

LABEL(noop ): TXCODE_noop (aTHX_ st); goto *(st->pc->exec_code);
LABEL(move_to_sb ): TXCODE_move_to_sb (aTHX_ st); goto *(st->pc->exec_code);
LABEL(move_from_sb ): TXCODE_move_from_sb (aTHX_ st); goto *(st->pc->exec_code);
LABEL(save_to_lvar ): TXCODE_save_to_lvar (aTHX_ st); goto *(st->pc->exec_code);

LABEL(end): TXCODE_end(aTHX_ st);

где исполнение «прыгает» между goto, пока не встретится опкод «end». Фактически, не происходит ненужных вызовов функций вообще, если, например, код TXCODE_move_to_sb выглядит как register.a = register.b, что позволяет вcе тело процессора теоретически уместить в процессорном кэше (практически это нереально, т.к. шаблоны не состоят только из простых действий – сплошь и рядом требуются вызовы перловых функций).

Со стороны парсера все выглядело тоже занимательно. Лично мне механика парсинга, все эти LL/LR/LALR парсеры – никогда не были особо интересны, но оценить идею я в состоянии. Как TT, так и Xslate, представляют собой небольшие самостоятельные языки, а не только набор директив (например, на Xslate можно «гольфить» вот эдак:
Hello, <: $lang // «Perl» :> world!

Однако ТТ использовал старый проверенный метод lex/yacc в исполнении Parse::Yapp, который лично мне импонирует более всего. Xslate использовал свой парсер, сделанный по типу top-down parser, и с которым я раньше не сталкивался. Я не буду вдаваться в подробности здесь, скажу только, что расширение синтаксиса с этим типом парсера было далеко не самой приятной задачей из моего опыта. Главным образом, из-за того, что многие части кода были общими для всех синтаксисов, поддерживаемых Xslate, и приходилось разбирать на части каждый из них.

Однако самые большие проблемы были еще впереди, в рантайме. Как оказалось, у Xslate в рантайме просто-напросто нет аналогичного TT-шному [% CALL %] механизма, который может вызывать функцию, определенную в другом шаблоне. С этим я нашел, как бороться, потратив пару недель свободного времени в ковырянии в его псевдо-ассемблере и пытаясь понять его принципы, чтобы вдруг чего не нарушить. Выяснилось также, что у ТТ есть возможность использовать несимметричные скобки, например, [%-… %], а парсер Xslate этого не умеет. Но самым большим камнем преткновения оказалось то, что у ТТ и у Xslate просто очень разные scope visibility для переменных, т.е. если в TT я пишу [% foo = 'bar' %] и после этого переменная foo видна во всех субшаблонах, в Xslate это локальная переменная.

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

С этим все оказалось просто – Devel::NYTProf, отличный профайлер (после многих лет с Devel::DProf особенно), выдал вот такую интересную картинку при трассировке шаблона:

image

Таблица была еще раз в пять длиннее, и я убрал некоторые строки для наглядности, но даже беглый анализ говорит о том, что основная нагрузка возникает не из-за ТТ, а независимо от него. Как следствие, даже если сделать так, что скорость от замены ТТ на Xslate будет нулевая, то и это не даст желаемого двукратного увеличения. Тем не менее, чтоб не отступать уж совсем с пустыми руками, я решил еще покопаться, и вот что обнаружил:

• В main_menu.inc и index.html мной был обнаружен банально медленный код вида

[% FOR x %]
[% FOR y %]
… do something…
[% END %]
[% END %]

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

• Затем – Template::Stash, часть ТТ. Этот главный подозреваемый, оказывается, давно и прочно был заоптимизирован дальше некуда в Template::Stash::XS – видимо, когда-то давно его скорость тоже кому-то мешала. Тем не менее, проблематична была именно его перловая часть, которую я, немного урезав, спустил с 356ms до 238ms. Проблема состоит только в том, что моя «урезка» работает конкретно для этого заказчика и с конкретно этим профилем, а патч для общего пользования не будет иметь смысла. Другими словами, как локальный хак применить можно, но потом сисадмину придется накладывать его каждый раз. Сомнительная выгода получается, в общем. Кому интересно, вот он:

— Template-Toolkit-2.24/xs/Stash.xs.0 2012-11-22 23:26:54.670467300 +0100
+++ Template-Toolkit-2.24/xs/Stash.xs 2012-11-22 23:26:39.406594300 +0100
@@ -1198,21 +1198,7 @@
}

if (!SvOK(RETVAL)) {
— dSP;
— ENTER;
— SAVETMPS;
— PUSHMARK(SP);
— XPUSHs(root);
— XPUSHs(ident);
— PUTBACK;
— n = call_method(«undefined», G_SCALAR);
— SPAGAIN;
— if (n != 1)
— croak(«undefined() did not return a single valuen»);
— RETVAL = SvREFCNT_inc(POPs);
— PUTBACK;
— FREETMPS;
— LEAVE;
+ RETVAL = newSVpv("", 0);
}
else

Грубо говоря, вызов undefined слишком дорог, а нужно всего лишь возвратить пустую строку. Но, если это будут 60 тысяч вызовов, то соответственно увеличится затрачиваемое время.

• IO::Uncompress::* и IO::Compress::*, как выяснилось, имеют отношение к memcached – он сжимает данные, если их объем превышает сколько-то там мегабайт по умолчанию. Я поигрался с этой опцией, но улучшение было микроскопическим.

• Наконец, Template::Iterator, 180660 вызовов. Я переписал его частично на XS и спустил с 263 до 7ms, это 5-7 % от всего запроса. Результат выложил на CPАN как Template::Iterator::XS. Это единственная вещь, которая может послужить на пользу сообществу. Если у вас есть существующий проект на ТТ, то вы вполне можете этот модуль встроить вот так

use Template;
use Template::Iterator::XS;
$Template::Config::ITERATOR = 'Template::Iterator::XS';

и посмотреть, что из этого выйдет.

Результаты оказались таковы, что ими похвастаться не получилось. Вопрос, как лучше всего (да и стоит ли вообще) переходить с ТТ на что-либо другое, остается для меня пока открытым. Более того, нельзя вынести однозначное заключение, что ТТ сильно плох – во всяком случае, по скорости выполнения, но это уже скорее вопрос для общего обсуждения.

Наконец, хочу поблагодарить REG.RU за возможность покопаться в интересном коде и за настойчивость в публикации модуля Template::Iterator::XS – результатом чего стала эта статья, которая по моей инициативе вряд ли увидела бы свет. Обычно компании откровенно не заинтересованы в том, чтобы код, который был написан по их заказу, публиковали где-либо. В этом плане REG.RU оказался приятным исключением, за что я, от лица перлового сообщества, выражаю признательность».

Автор: nastik

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js