Comments 25
#undef PEEK_ARG
должно быть
#undef NEXT_ARG
для удаления объявленного ранее макроса.
Раз уж на то пошло, тогда уж разумней сделать парочку static inline функций, лямбы тут совершенно ни к чему.
Там зачем там лямбды? :-)
— в коде не хватает проверок на `#ifdef NEXT_OP ...`
Не, я не спорю, макросы иногда очень выручают, но данный код был бы гораздо лучше без них. Тем более отказавшись от них, код, как мне кажется, стал бы чуть более безопасным и выразительным.
Наверное, в плюсах я бы подумал над лямбда-функциями, но… Просто маленькие функции, которые легко заинлайнятся, тут сработали бы даже лучше.
В практической реализации при добавлении потоков инструкцией SPLIT или любым другим способом мы будем проверять, нет ли уже потока с данной инструкцией. Добавлять новый не имеет смысла — у него то же самое состояние (единственный указатель на текущую инструкцию).
То есть при длине сессии N и длине байт-кода M мы будем иметь максимум M потоков. Для целой сессии мы максимум O(N*M) раз побеспокоим наши потоки.
Однако, если, допустим, добавить инструкции SAVE, сохраняющие массив, почему вдруг мы не можем извлечь подгруппы..?
Вы не против, если я подумаю вслух?
У потока, строго говоря, три значения в контексте: instruction pointer, текущее событие, массив подгрупп. На каждом шаге обработки потоков текущее событие будет одинаково. Различаются IP и массивы подгрупп.
Пускай у нас регулярное выражение вида "(a*)b", которое соответствует байт-коду
SAVE 1
splitlabel:
SPLIT label1 label2
label1:
NEXT
EVENT a
JUMP splitlabel
label2:
SAVE 2
NEXT
EVENT b
MATCH
Предположим, мы матчим строку aab. На втором шаге у нас будет два потока, один из которых уже сматчил первое событие a. Надо добавить второй поток, у котрого тот же IP и будет то же текущее событие. Разница в том, что первый поток уже сохранил историю в виде SAVE 1. Дальше они будут работать одинаково.
То есть у потоков разная история, но одинаковое будущее. Выходит, что мы
, к примеру, можем решить, какая история потоков нас будет интересовать. Например, более длинная группа будет выигрывать.
Я согласен с вами в том смысле, что тут масса тонкостей, но по крайней мере часть из вопросов решить можно.
В простом случае, который я с самого начала указал (нет подгрупп, просто не больше одного потока на каждый конкретный IP), такой недетерминированный автомат соответствует конечному, и это легко показать: пускай каждое состояние недетерминированного автомата в виде распределения потоков по разным IP соответствует одному состоянию конечного автомата. Обозначим его числом 1.Каждое следующее событие будет как-то менять распределение событий, и будет это делать предсказуемым способом. То есть мы можем и это новое состояние тоже поставить в соответствие одному распределению потоков, и обозначить числом. И т.д.
Словом, это один из стандартных методов построения DFA из NFA.
А вот если мы хотим и группы сохранить, и еще всякую логику выбора интересных нам историй прикрутить, то, конечно, DFA не будет, но вот прям страстей по потреблению памяти можно попробовать избежать.
А вы интересуетесь темой?
Стандартный способ построения DFA из NFA — это каждому множеству состояний NFA сопоставить одно состояние нового DFA. В общем случае, если у NFA n состояний, то у DFA их будет 2^n (кол-во подмножеств множества 1..n) и потребление памяти на хранение номера состояния будет расти экспоненциально.
Если в лоб сконвертировать NFA, получится DFA с большим кол-вом состояний и переходов, которые в явном виде хранить будет тяжко. А тут выходит какая-то хитрость, что состояние DFA не является классическим числом, а размазано по нескольким переменным (списку потоков), и это позволяет уменьшить накладные расходы. Этот подход интересно изучить подробнее, всегда ли можно его применять.
В данном случае, опять же если не обсуждать вопрос групп и других вещей, которые очень легко делать именно на NFA, то это называется «создание DFA на лету». Т.е. мы в каком-то смысле можем закешировать те состояния, при не обязательно сразу строить весь автомат.
Если кеш — пара ключ-значение, то ключом будет пара из состояния списка потоков и входного события, а значением — следующее состояние списка потоков. Если пронумеровать состояния списка потока, и построить эти состояния для всех корректных входных событий, то получим DFA в кеше.
Профит — не обязательно строить полный DFA, можно просто частично его генерировать по мере необходимости.
Но лично меня интересует не DFA, а возможность слегка облегчить страдания NFA, на которую мы тут с вами обратили внимание.
Вообще, вот здесь очень много об этом и не только: swtch.com/~rsc/regexp
Обратите внимание на пункт «Caching the NFA to build a DFA» в первой части статьи.
Например, регулярное выражение (a|a|a)* и входная строка aaaaaaaaaaa… будут утраивать число потоков на каждом шаге.
Опять же, предложенная оптимизация здесь будет работать, нет?
(a|a*)*
уже не сожмёшь, потому что формально подвыражения неодинаковые.Эт точно.
Ну, тут как с оптимизациями в компиляторах в целом. Мы можем, например, частично решить проблему, приведя дерево к какой-нибудь канонической форме, где часть таких случаев будет сведена к единственному варианту.
Но все равно будет какая-нибудь засада, да. В нашей системе все устроено так, что обработка регулярки происходит в процессе-воркере, у которого есть четкие ограничения на ресурсы. При превышении потребления ресурсов процесс будет убит, система же в целом продложит работу.
Иголка в стоге сессий, или Байт-код регулярных выражений