<regex>: Avoid stack growth in simple loops
#5939
Merged
+161
−32
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
To recap from #5889, simple loops have the following properties:
The matcher has always used properties 1 and 2 of such loops. It also took slight advantage of property 3: As a corollary, all repetitions match the empty string iff the first repetition matches the empty string, so the matcher only checked whether the first repetition is empty.
But properties 3 and 4 can be exploited further: If we know that each successful repetition shifts the matched string and the capturing groups by the same distance, we do not have to explicitly store these positions on the stack for unwinding greedy matching but can restore the positions while backtracking by shifting the positions in the other direction by the same amount. As for non-greedy matching, failing to match the next repetition will immediately result in backtracking beyond the first repetition, so we actually do not even have to know the length of the strings matched by each repetition, but only have to allow backtracking to proceed for the first stack frames that were pushed while matching the first repetition.
At least for greedy matching, though, we can't easily avoid that these stack frames are pushed while matching the next repetition, because they are still needed to restore the match state when backtracking from the last attempted match.
This PR implements that stack frames pushed while matching a repetition are popped afterwards from the stack without any further processing from the second repetition on. Thus, the stack stops growing while the matcher processes the simple loop.
This is probably the most intricate PR since the start of the non-recursive matcher PR, because it keeps tampering with this stack and does not just pop from the stack, but even repeatedly modifies two special stack frames that were pushed earlier while processing the loop's
_N_repand_N_end_repnodes. However, I believe the performance benefit for simple loops is worth this complication (especially because I hope to extend this optimization to even more loops that are currently not marked simple).The two special stack frames (that can be recognized in the code by the assignment of opcode _Do_nothing to them in some cases) are used as follows:
_Loop_vals[_Node->_Loop_number]._Loop_frame_idx) stores the initial position in the searched string at the start of the loop. If matching is greedy and the minimum number of repetitions is zero, the opcode is_Loop_simple_greedy_firstrep(to set up tail matching if even matching the first repetition fails), otherwise it is_Do_nothing. Backtracking during non-greedy matching on the other hand is handled by pushing an additional stack frame with opcode_Loop_simple_nongreedy._Loop_frame_idx_savmember, while_Loop_vals[_Nr->_Loop_number]._Loop_frame_idxpoints to the second stack frame. The second stack frame's iterator is generally changed to point to the current position in the input string (except when its opcode is_Do_nothing). The frame's code is assigned as follows:_Loop_simple_greedy_firstrepassigned), the code of the second frame is initialized to_Loop_simple_greedy_lastrep._Do_nothing._Loop_simple_greedy_lastrep._Loop_simple_nongreedy(each time, because the contents might have been overwritten in-between).Meanwhile, the position in the first stack frame is used during backtracking from greedy matching to indicate when we have backtracked beyond the second repetition or the minimum number of repetitions. It is set to the start position of the first repetition or the repetition whose match resulted in reaching the minimum. If these positions in the input string are reached while backtracking successive repetitions of the loop, the backtracking logic for non-initial repetitions is stopped and the normal stack unwinding logic is allowed to proceed again.
Because positions must be shifted back during greedy matching, the iterators of the input string must be decremented during backtracking (to either calculate the position where the previous repetition stopped or to move the start and end positions of capturing groups accordingly). The standard requires that provided iterators must be bidirectional, so the matcher must always be able to perform such decrements. But I think the matcher has only required forward iterators in practice before this PR and I think the matcher will enter an endless after this PR if assertions are disabled (because
std::advance()will just not shift the iterator by a negative distance if the iterator isn't bidirectional). For this reason, this PR also adds static assertions checking the bidirectional iterator requirement.Individual changes
regex_match(),regex_search()andregex_replace()._Rep_lengthto (renamed)_Loop_vals_v3_t, which will hold the length of the first (and thus every) repetition for simple loops after matching the first repetition. The storage is now templated on the difference type of the input string iterator._N_rep, merge the_Loop_simple_greedystack frame into the previously pushed stack frame with code_Do_nothingby changing the code of the former to_Loop_simple_greedy_firstrep._N_end_repfor simple loops:_Sav._Loop_frame_idx(while storing the position of the first special one to_Frame._Loop_frame_idxin this second frame). The code of the second stack frame is initialized to_Loop_simple_greedy_lastrepif the first one's code is_Loop_simple_greedy_firstrep(i.e., backtracking from greedy matching might happen until the very first repetition), else it's set to_Do_nothingfor now._Frames_countto_Sav._Loop_frame_idx + 1, keeping only the second special frame around._Do_nothingyet). If so, set the iterator of the first special stack frame (with code_Do_nothingas well) to the start of the prior repetition and change the second stack frame's code to_Loop_simple_greedy_lastrep._Loop_simple_greedy_intermediaterep._Framesvector, so we can avoid calling_Push_frame(). However, we have to reset its members as necessary because its contents might have been overwritten.)_Loop_simple_greedythe one for_Loop_simple_greedy_firstrep._Loop_simple_greedy_lastrepfor the handler of_Loop_simple_greedy_lastrepand add the logic to set up the stack frame for unwinding to the prior repetition in case of match failure._Loop_simple_greedy_intermediaterepbefore_Loop_simple_greedy_lastrep, add code to shift the start and end iterators of the capturing groups. The capturing groups matched by each repetition are identified by walking the stack frames between the first and second special stack frame. After adjusting the capturing groups, fall through to the_Loop_simple_greedy_lastrephandler.Tests
The tests verify that backtracking from loops still works and capturing groups are set correctly despite these intricate stack manipulations. Backreferences are used to verify the contents of the capturing groups.
In the non-greedy case, failing to match a single repetition means that the loop is backtracked from completely. This is why a single test case verifying that the capturing group is unmatched is sufficient here.
In the greedy case, we have to verify that three different opcodes are handled correctly during unwinding, and backtracking after failing the last attempted repetition might stop at any repetition in-between. Moreover, special handling is necessary when the maximum number of repetitions is reached or when backtracking beyond the second or the minimum repetition. The tests are chosen to provide coverage for all these cases.
Benchmark