Repository navigation
Conversation
pythongh-152214 added a set-operation fusion pass to the pattern compiler. optimize() walks the whole parsed tree unconditionally, so patterns that contain no fusable construct pay for a full extra pass and get nothing in return. On main this makes re.compile() about 15-20% slower than 3.15. Have the parser record whether the tree holds a construct that the fusion pass can rewrite, and walk only in that case. The marker stays None for trees that were not produced by parse() (such as the ones re.Scanner assembles by hand), so those are still optimized as before.
|
Thank you for your interest in contributing to Python. This is a community-driven project, and we want it to be a welcoming place to learn and collaborate. Contributors are responsible for reviewing and understanding what they submit, checking its claims, and being able to explain changes in their own words. This helps reviewers focus on the contribution and leaves more time to support other contributors. We believe this contribution does not meet our guidelines for using AI tools, so we're closing it. If you think we've misunderstood your contribution, please let us know. You're welcome to explain your approach or ask questions about the guidelines. Please read our guidelines for using AI tools. |
|
A core dev is more likely to address this. |
But do you think you're right? |
|
The issue hasn't been open for enough time and no consensus on how to fix this has been made, so yes, a core dev must first acknowledge the direction of the fix. |
Summary
re.compile()on main is about 16-19% slower than 3.15 for ordinarypatterns. The regression comes from gh-152214, which added a set-operation
fusion pass to the pattern compiler (
re._optimizer). That pass walks thewhole parsed tree unconditionally, looking for
||,&&and--constructs. Patterns without those constructs pay for the walk and get
nothing back.
This PR lets the parser record whether the tree contains anything the
fusion pass can actually rewrite, and skips the walk when it does not.
Root cause
Since gh-152214,
re._optimizer.optimize()always calls_walk():_walk()recurses over every node of the parsed tree and calls thepeepholes (
_fuse_difference, ...) on each node. For a pattern like\d{4}-\d{2}-\d{2}there is nothing to fuse: the walk visits the wholetree and rewrites nothing. Profiling
re.compile()over a corpus of 110distinct patterns collected from the stdlib and site-packages shows no
pattern that the fusion pass rewrites.
Approach
The parser is the only place that knows whether a fusable construct is
present, so it records that in
State.fusable:State.fusablestarts asNone, meaning "unknown": the tree was notproduced by
parse()(for example,re.Scannerassembles one by hand).The optimizer must still walk those trees.
parse()sets it toFalse, and the parse paths that can producesomething the optimizer understands set it to
True:|betweenone-character alternatives (
_parse_sub,_union),--(_difference)and negative lookbehind (
(?<!...)).optimize()walks only when the marker is notFalse.Behaviour is unchanged: for 1504 patterns (both
strandbytes, takenfrom the stdlib, site-packages and
test_re), the bytecode produced bymain and by this branch is identical, and the same 8 patterns fail to
compile with the same errors.
Skipping the walk based on a plain boolean would change behaviour for
hand-assembled trees:
re.Scannerbuilds patterns without callingparse(), and those still need optimization. The tri-state marker keepsthat path working.
Benchmarks
The official pyperformance
regex_compilebenchmark, run withpyperfon Release builds, CPU affinity pinned to one core, Windows 11,
i5-12450H, CPython 3.16.0a1+ (
a497a11c46):a497a11c46Paired by run, this branch is 1.16x-1.19x faster than main and back in
the range of 3.15.2. The machine is noisy (the 3.15.2 runs differ by 25%
between themselves), so I would not read anything into the remaining
few percent.
Tests
python -m test test_re: 173 tests, 4 skipped, all pass.test_optimizer_walk_only_for_fusable_patternspins theparser marker for fusable and non-fusable patterns, and checks that a
hand-built
State()keeps the "must optimize" default.AI assistance
Developed with AI assistance for the analysis, the patch draft and the
benchmark scripts. I reviewed every line, ran the tests and benchmarks
myself, and can explain the change in my own words.