serhiy-storchaka · GitHub

The character class escapes \d, \D, \s, \S, \w and \W are currently always compiled to an IN block containing a single CATEGORY item, even when they occur outside a character set:

>>> re.compile(r'\d', re.DEBUG)
...
 5: IN 4 (to 10)
 7.   CATEGORY UNI_DIGIT
 9.     FAILURE
10: SUCCESS

The IN wrapper costs three extra code words (IN, the skip, and FAILURE), an extra dispatch, and an indirect SRE(charset) call per character — even though there is only a single alternative.

A category escape that appears outside a set can instead be compiled directly to a bare CATEGORY opcode:

>>> re.compile(r'\d', re.DEBUG)
...
 5: CATEGORY UNI_DIGIT
 6: SUCCESS

This also makes such an escape a "simple" repeatable unit, so \d+ uses the REPEAT_ONE fast path (handled by SRE(count)) instead of the generic REPEAT/MAX_UNTIL loop; a CATEGORY case is added to SRE(count) accordingly.

The transformation preserves behaviour exactly (the engine already matched the same category) and only changes the compiled byte code.

In a release build I measure ~1.3x geometric-mean speedup across a range of category-heavy patterns — roughly 1.7–2.0x on scans like \d+, \s+, \S+, and ~1.1–1.2x on realistic tokenizing, date, and IP-address patterns — together with ~20% smaller compiled byte code for those patterns. Patterns that do not use bare category escapes are unaffected.

Linked PRs

Read the original on github.com ↗