Thread
Commits
-
Improve performance of regular expression back-references.
- 0c3405cf11a1 14.0 landed
-
Fix semantics of regular expression back-references.
- 4aea704a5bfd 14.0 landed
-
Allow condition variables to be used in interrupt code.
- f5a5773a9dc4 14.0 cited
-
Regex back-reference semantics and performance
Tom Lane <tgl@sss.pgh.pa.us> — 2021-03-01T00:53:49Z
I noticed that some of the slowest cases in Joel's regex test corpus had issues with back-reference matching, and along the way to fixing that discovered what seems to me to be a bug in the engine's handling of back-references. To wit, what should happen if a back-reference is to a subexpression that contains constraints? A simple example is SELECT regexp_match('foof', '(^f)o*\1'); To my mind, the back reference is only chartered to match the literal characters matched by the referenced subexpression. Here, since that expression matches "f", the backref should too, and thus we should get a match to "foof". Perl gives that answer, anyway; but our existing code says there's no match. That's because it effectively copies the constraints within the referenced subexpression, in addition to making the data comparison. The "^" can't match where the second "f" is, so we lose. 0001 attached fixes this by stripping constraint arcs out of the NFA that's applied to the backref subre tree node. Now, as to the performance issue ... if you load up the data in "trouble.sql" attached, and do SELECT regexp_matches(subject, pattern, 'g') FROM trouble; you'll be waiting a good long time, even with our recent improvements. (Up to now I hadn't tried the 'g' flag with Joel's test cases, so I hadn't noticed what a problem this particular example has got.) The reason for the issue is that the pattern is (["'`])(?:\\\1|.)*?\1 and the subject string has a mix of " and ' quote characters. As currently implemented, our engine tries to resolve the match at any substring ending in either " or ', since the NFA created for the backref can match either. That leads to O(N^2) time wasted trying to verify wrong matches. I realized that this could be improved by replacing the NFA/DFA match step for a backref node with a string literal match, if the backreference match string is already known at the time we try to apply the NFA/DFA. That's not a panacea, but it helps in most simple cases including this one. The way to visualize what is happening is that we have a tree of binary concatenation nodes: concat / \ capture concat / \ other stuff backref Each concat node performs fast NFA/DFA checks on both its children before recursing to the children to make slow exact checks. When we recurse to the capture node, it records the actual match substring, so now we know whether the capture is " or '. Then, when we recurse to the lower concat node, the capture is available while it makes NFA/DFA checks for its two children; so it will never mistakenly guess that its second child matches a substring it doesn't, and thus it won't try to do exact checking of the "other stuff" on a match that's bound to fail later. So this works as long as the tree of concat nodes is right-deep, which fortunately is the normal case. It won't help if we have a left-deep tree: concat / \ concat backref / \ capture other stuff because the upper concat node will do its NFA/DFA check on the backref node before recursing to its left child, where the capture will occur. But to get that tree, you have to have written extra parentheses: ((capture)otherstuff)\2 I don't see a way to improve that situation, unless perhaps with massive rejiggering of the regex execution engine. But 0002 attached does help a lot in the simple case. (BTW, the connection between 0001 and 0002 is that if we want to keep the existing semantics that a backref enforces constraints, 0002 doesn't work, since it won't do that.) regards, tom lane -
Re: Regex back-reference semantics and performance
Joel Jacobson <joel@compiler.org> — 2021-03-01T11:13:56Z
On Mon, Mar 1, 2021, at 01:53, Tom Lane wrote: >0001-fix-backref-semantics.patch >0002-backref-performance-hack.patch I've successfully tested both patches. On HEAD the trouble-query took forever, I cancelled it after 23 minutes. HEAD (f5a5773a9dc4185414fe538525e20d8512c2ba35): SELECT regexp_matches(subject, pattern, 'g') FROM trouble; ^CCancel request sent ERROR: canceling statement due to user request Time: 1387398.764 ms (23:07.399) HEAD + 0001 + 0002: SELECT regexp_matches(subject, pattern, 'g') FROM trouble; Time: 24.943 ms Time: 22.217 ms Time: 20.250 ms Very nice! I also verified the patches gave the same result for the performance_test: SELECT is_match <> (subject ~ pattern) AS is_match_diff, captured IS DISTINCT FROM regexp_match(subject, pattern, flags) AS captured_diff, COUNT(*) FROM performance_test GROUP BY 1,2 ORDER BY 1,2 ; is_match_diff | captured_diff | count ---------------+---------------+--------- f | f | 3360068 (1 row) No notable timing differences: HEAD (f5a5773a9dc4185414fe538525e20d8512c2ba35) Time: 97016.668 ms (01:37.017) Time: 96945.567 ms (01:36.946) Time: 95261.263 ms (01:35.261) HEAD + 0001: Time: 97165.302 ms (01:37.165) Time: 96327.836 ms (01:36.328) Time: 96295.643 ms (01:36.296) HEAD + 0001 + 0002: Time: 96447.527 ms (01:36.448) Time: 94262.288 ms (01:34.262) Time: 95331.483 ms (01:35.331) /Joel
-
Re: Regex back-reference semantics and performance
Tom Lane <tgl@sss.pgh.pa.us> — 2021-03-01T20:22:08Z
"Joel Jacobson" <joel@compiler.org> writes: > On Mon, Mar 1, 2021, at 01:53, Tom Lane wrote: >> 0001-fix-backref-semantics.patch >> 0002-backref-performance-hack.patch > I've successfully tested both patches. Again, thanks for testing! > On HEAD the trouble-query took forever, I cancelled it after 23 minutes. Yeah, I have not had the patience to run it to completion either. > No notable timing differences: I'm seeing a win of maybe 1% across the entire corpus, which isn't much but it's something. It's not too surprising that this backref issue is seldom hit, or we'd have had more complaints about it. BTW, I had what seemed like a great idea to improve the situation in the left-deep-tree case I talked about: we could remember the places where we'd had to use the NFA to check a backreference subre, and then at the point where we capture the reference string, recheck any previous approximate answers, and fail the capture node's match when any previous backref doesn't match. Turns out this only mostly works. In corner cases where the match is ambiguous, it can change the results from what we got before, which I don't think is acceptable. Basically, that allows the backref node to have action-at-a-distance effects on where the earlier concat node divides a substring, which changes the behavior. So it seems this is about the best we can do for now. I'll wait a little longer to see if anyone complains about the proposed semantics change, though. regards, tom lane