ANYKS
RU
in developmentpart of AWH · 51,160 lines

awh::regex and Grok

Our own regular expression engine with machine code generation, and a log-parsing layer on top of it. It was written to remove PCRE2 from AWH — and it did: the standard build doesn't know about it, and the reference stays only for verification.

01 In brief

Faster than PCRE2 almost everywhere — and several times faster on Elbrus

37 of 41scenarios where our machine code beats PCRE2's machine code; median ratio 1.18
0 of 45scenarios where our interpreter is behind PCRE2's interpreter; median ratio 1.46
×6.6on Elbrus: our code vs PCRE2, which has no e2k code at all
307built-in Grok patterns
02 Design

Five ways to apply an expression to text

The engine picks a strategy from the properties of the expression and the text rather than using one for everything.

StrategyWhenWhat it does
Prefilterbefore any automatonthe set of admissible first bytes and a required literal: stretches of text are skipped without running the automaton
DFAa yes/no answer is enoughstates are built on demand, one pass over the text
Pikegroup boundaries are neededall states at once, captures in linear time — no blow-up on crafted expressions
Backtrackoutside the regular subsetbackreferences, lookahead and lookbehind, atomic groups, recursion
CodegenARM64, x86-64 and e2kthe expression program is turned into machine code

A compiled expression never changes after compilation and is shared between threads without locks; each thread has its own matching state.

03 Code generation

Relocatable code — decided before the first line

Everything outside the generated code — tables, parsing subroutines, character classes — is reached by an offset from a single pointer. The code contains no absolute addresses, so it is stored together with the expression: on a million expressions that is twelve seconds not spent at startup.

A memory region is never writable and executable at once; on Apple ARM64 via MAP_JIT, on OpenBSD with BTI landing pads.

The gain of code over our own interpreter shrinks with every interpreter fix, and that is a measure of success: it used to reach ×153, now the largest is ×34.7. The interpreter has caught up with the code where it used to lag tens of times behind.

ExpressionCodegen gain
\w+(?=@)×34.7
(\w+)@(\w+)\.(\w+)×32.6
(?>\w+)@\w+×28.0
.*?needle×14.0
Content-Lengthprefilter leads
04 Benchmarks

Forty-five scenarios, two measures

Matches per second; ARM64 (Apple M4 Max), both sides built with -O3. Two pairs are compared separately: our machine code against PCRE2's machine code, and our interpreter against its interpreter. They must not be mixed: the single measure we used before — code against code — hid the interpreter's lag, and it only surfaced on Elbrus.

Machine code vs machine codeAWHPCRE2Ratio
.*needle113,3148,93512.68
(?>\w+)@\w+9,086,4322,429,4203.74
(?:[a-z]* ?)*dog42,179,84111,491,4793.67
alpha|bravo|charlie|delta|echo|foxtrot11,4056,2011.84
^(GET|POST) (\S+) HTTP/(\d)\.(\d)$105,297,79559,989,5131.76
(\w+) \17,014,3644,372,8191.60
\((?:[^()]|(?R))*\)28,721,84925,997,9301.10
(?:HT|TP)/171,259,96584,772,7030.84
Interpreter vs interpreterAWHPCRE2Ratio
(?:fox|dog)trap212,3902,62980.77
\bneedle\b210,04610,32920.33
.*needle73,0765,10414.32
Content-Length122,768,37924,091,3065.10
\((?:[^()]|(?R))*\)2,409,0101,652,5451.46
(?:[a-z]+/)+v116,252,67322,992,9130.71

Machine code: faster than PCRE2 in 37 of 41 scenarios; of the remaining four, three are at 0.99, and only (?:HT|TP)/1 is noticeably behind; median ratio 1.18. Interpreter: ahead in 21 of 45, level in 24, behind in none; median ratio 1.46. "Ahead" means a ratio of 1.5 or more, "behind" means below 0.67. Compiling an expression costs us 2.3× more (0.44 of PCRE2): we build two programs — forward and reverse — and analyse the expression more deeply; it is compiled once and applied millions of times.

05 Elbrus

Machine code for Elbrus, which PCRE2 doesn't have

The code generator speaks the e2k instruction set (Elbrus-8C2, lcc): 23 back-end methods, with the encoder checked against the machine's assembler byte for byte. PCRE2 cannot generate code for this architecture at all, so here our code is compared with its interpreter.

ExpressionOur codePCRE2Ratio
(?:fox|dog)trot3,75310934.4
\w+(?=@)132,2495,76522.9
(?>\w+)@\w+210,07412,33017.0
(\w+)@(\w+)\.(\w+)17,2231,03016.7
.*needle3,00721613.9
[0-9]{3,5}1,032,2211,131,0700.91

Overall: ahead in 39 of 41 scenarios, median ratio 6.6. Interpreter against interpreter: ahead in 22, level in 23, behind in none.

06 Store

Compiled expressions without recompiling

Memory image

The expression program is written as is; restoring it is setting a view over the record, with no allocation or copying.

Distrust of the record

Every jump target, class, cell and group number is checked: a corrupted record is a failure, not a walk through memory.

Encryption and expiry

Encryption and compression via consumer handlers, record lifetime, machine and instruction-set identification.

07 Correctness

Twenty verification stands and one deliberate difference

StandComparisonsMismatches
Unicode properties, 1,225 properties1,362,278,4000
shorthand character classes26,689,5360
match boundaries on an expression corpus112,2610
verdict and reverse pass236,4980
acceptance and rejection of expressions200,000 patterns0
grapheme clusters48,218,62469,131

Graphemes follow the current edition of UAX #29, while PCRE2's table lags it in three rules. Two separate stands show the difference is limited to those rules: both report zero mismatches on the same 48 million comparisons.

08 Grok

Log parsing with named patterns

A pattern registry, expansion of %{NAME:field:type} references into a regular expression, and field extraction to JSON.

grok.build("%{IP:client} %{WORD:method} %{URIPATHPARAM:request} %{INT:code:int}");

// 192.168.1.10 GET /api/v1/orders?id=17 200
{"client":"192.168.1.10","method":"GET","request":"/api/v1/orders?id=17","code":200}
  • References expand as text, not as subroutine calls: the compiler sees the whole tree, so both first-byte filtering and code generation work.
  • Field names are wider than group names — hyphens, dots, repeats: %{IP:src-ip}:%{INT:src.port}.
  • Numbers only by declared type. A version number or a code with a leading zero stays text, and the output stays valid JSON.
  • Errors fail the build: unknown patterns and circular references never pass silently.

Grok patterns are also available in the online converter ACU: turn a log line into JSON, XML or YAML right in the browser.