From 3d257f06a3f8e951479abb0623c6ac0dfafe7d26 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Mon, 29 Jul 2024 21:05:26 -0400 Subject: [PATCH] Remove some overhead from SymbolcRegexMatcher - Avoid try/finally blocks, instead using breaks to break out of a loop to a single return - Consolidate two almost identical methods for DFA transitions - Avoid an unnecessary GetPositionKind call that could instead use a hardcoded index - Avoid a duplicate read for a ref parameter - Outline a throw --- .../Symbolic/MintermClassifier.cs | 1 + .../Symbolic/SymbolicRegexMatcher.cs | 424 ++++++++---------- 2 files changed, 191 insertions(+), 234 deletions(-) diff --git a/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/Symbolic/MintermClassifier.cs b/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/Symbolic/MintermClassifier.cs index bf4735f2bf1459..569f5f8fdfba33 100644 --- a/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/Symbolic/MintermClassifier.cs +++ b/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/Symbolic/MintermClassifier.cs @@ -110,6 +110,7 @@ public int GetMintermID(int c) return (uint)c < (uint)lookup.Length ? lookup[c] : 0; } } + /// /// Gets a quick mapping from char to minterm for the common case when there are <= 255 minterms. /// Null if there are greater than 255 minterms. diff --git a/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/Symbolic/SymbolicRegexMatcher.cs b/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/Symbolic/SymbolicRegexMatcher.cs index e67a72018dfd57..e7f1b842326dce 100644 --- a/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/Symbolic/SymbolicRegexMatcher.cs +++ b/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/Symbolic/SymbolicRegexMatcher.cs @@ -320,15 +320,13 @@ internal TSet GetMintermFromId(int mintermId) { TSet[] minterms = _minterms; - // A minterm ID of minterms.Length represents a \n at the very end of input, which is matched - // by the \Z anchor. - if ((uint)mintermId >= (uint)minterms.Length) + if ((uint)mintermId < (uint)minterms.Length) { - return _builder._newLineSet; + return minterms[mintermId]; } - // Otherwise look up the minterm from the array - return minterms[mintermId]; + // A minterm ID of minterms.Length represents a \n at the very end of input, which is matched by the \Z anchor. + return _builder._newLineSet; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -342,8 +340,10 @@ private void CheckTimeout(long timeoutOccursAt) Debug.Assert(_checkTimeout); if (Environment.TickCount64 >= timeoutOccursAt) { - throw new RegexMatchTimeoutException(string.Empty, string.Empty, TimeSpan.FromMilliseconds(_timeout)); + ThrowRegexTimeout(); } + + void ThrowRegexTimeout() => throw new RegexMatchTimeoutException(string.Empty, string.Empty, TimeSpan.FromMilliseconds(_timeout)); } /// Find a match. @@ -541,6 +541,7 @@ private int FindEndPositionOptimized(R CheckTimeout(timeoutOccursAt); } } + return endPos; } @@ -628,104 +630,96 @@ private bool FindEndPositionDeltasDFAOptimized= lengthMinus1) + // If there is more input available try to transition with the next character. + // Note: the order here is important so the transition itself gets taken + if (!DfaStateHandler.TryTakeTransition(this, ref currStateId, GetMintermId(mintermsLookup, c), timeoutOccursAt) || + pos >= lengthMinus1) + { + if (pos + 1 < input.Length) { - if (pos + 1 < input.Length) - { - return false; - } - pos++; - - // one off check for the final position - // this is just to move it out of the hot loop - if (!(_stateFlagsArray[currStateId].IsNullable() || - _stateArray[currStateId]!.IsNullableFor(GetPositionKind(-1)))) - { - return true; + result = false; + break; + } - } + pos++; - // the end position (-1) was nullable + // One off check for the final position. This is just to move it out of the hot loop. + if (_stateFlagsArray[currStateId].IsNullable() || + _stateArray[currStateId]!.IsNullableFor(_positionKinds[0])) + { + // The end position (-1) was nullable. endPos = pos; - return true; } - // We successfully transitioned, so update our current input index to match. - pos++; + break; } + + // We successfully transitioned, so update our current input index to match. + pos++; } - finally - { - // Write back the local copies of the ref values. - posRef = pos; - endPosRef = endPos; - currentStateIdRef = currStateId; - } + + // Write back the local copies of the ref values. + posRef = pos; + endPosRef = endPos; + currentStateIdRef = currStateId; + + return result; } /// @@ -749,49 +743,48 @@ private bool FindEndPositionDeltasDFA CurrentState state = stateRef; int deadStateId = _deadStateId; int initialStateId = _initialStateId; - try + + // Loop through each character in the input, transitioning from state to state for each. + bool result = true; + while (true) { - // Loop through each character in the input, transitioning from state to state for each. - while (true) + if (state.DfaStateId == deadStateId || + (state.DfaStateId == initialStateId && !TInitialStateHandler.TryFindNextStartingPosition(this, input, ref state.DfaStateId, ref pos, null!))) { - if (state.DfaStateId == deadStateId || - (state.DfaStateId == initialStateId && !TInitialStateHandler.TryFindNextStartingPosition(this, input, ref state.DfaStateId, ref pos, null!))) - { - return true; - } + break; + } - int positionId = DefaultInputReader.GetPositionId(this, input, pos); + int positionId = DefaultInputReader.GetPositionId(this, input, pos); - // If the state is nullable for the next character, meaning it accepts the empty string, - // we found a potential end state. - if (TNullabilityHandler.IsNullableAt(this, in state, positionId)) + // If the state is nullable for the next character, meaning it accepts the empty string, + // we found a potential end state. + if (TNullabilityHandler.IsNullableAt(this, in state, positionId)) + { + endPos = pos; + if (mode == RegexRunnerMode.ExistenceRequired) { - endPos = pos; - // A match is known to exist. If that's all we need to know, we're done. - if (mode == RegexRunnerMode.ExistenceRequired) - { - return true; - } - } - - // If there is more input available try to transition with the next character. - if (pos >= length || !DfaStateHandler.TryTakeTransition(this, ref state.DfaStateId, positionId)) - { - return false; + break; } + } - // We successfully transitioned, so update our current input index to match. - pos++; + // If there is more input available try to transition with the next character. + if (pos >= length || !DfaStateHandler.TryTakeTransition(this, ref state.DfaStateId, positionId)) + { + result = false; + break; } + + // We successfully transitioned, so update our current input index to match. + pos++; } - finally - { - // Write back the local copies of the ref values. - posRef = pos; - endPosRef = endPos; - stateRef = state; - } + + // Write back the local copies of the ref values. + posRef = pos; + endPosRef = endPos; + stateRef = state; + + return result; } /// @@ -812,48 +805,40 @@ private bool FindEndPositionDeltasNFA( // To avoid frequent reads/writes to ref and out values, make and operate on local copies, which we then copy back once before returning. int pos = posRef; int endPos = endPosRef; - try - { - // Loop through each character in the input, transitioning from state to state for each. - while (true) - { - // Dead end here means the set is empty - if (state.NfaState!.NfaStateSet.Count == 0) - { - return true; - } - int positionId = DefaultInputReader.GetPositionId(this, input, pos); + // Loop through each character in the input, transitioning from state to state for each. + bool result = true; + while (state.NfaState!.NfaStateSet.Count != 0) // Dead end here means the set is empty + { + int positionId = DefaultInputReader.GetPositionId(this, input, pos); - // If the state is nullable for the next character, meaning it accepts the empty string, - // we found a potential end state. - if (TNullabilityHandler.IsNullableAt(this, in state, positionId)) + // If the state is nullable for the next character, meaning it accepts the empty string, + // we found a potential end state. + if (TNullabilityHandler.IsNullableAt(this, in state, positionId)) + { + endPos = pos; + if (mode == RegexRunnerMode.ExistenceRequired) { - endPos = pos; - // A match is known to exist. If that's all we need to know, we're done. - if (mode == RegexRunnerMode.ExistenceRequired) - { - return true; - } - } - - // If there is more input available try to transition with the next character. - if (pos >= length || !NfaStateHandler.TryTakeTransition(this, ref state, positionId)) - { - return false; + break; } + } - // We successfully transitioned, so update our current input index to match. - pos++; + // If there is more input available try to transition with the next character. + if (pos >= length || !NfaStateHandler.TryTakeTransition(this, ref state, positionId)) + { + break; } + + // We successfully transitioned, so update our current input index to match. + pos++; } - finally - { - // Write back the local copies of the ref values. - posRef = pos; - endPosRef = endPos; - } + + // Write back the local copies of the ref values. + posRef = pos; + endPosRef = endPos; + + return result; } /// @@ -921,46 +906,46 @@ private bool FindStartPositionDeltasDFA( // To avoid frequent reads/writes to ref values, make and operate on local copies, which we then copy back once before returning. int pos = i; CurrentState state = stateRef; - try - { - // Loop backwards through each character in the input, transitioning from state to state for each. - while (true) - { - int positionId = TInputReader.GetPositionId(this, input, pos - 1); - // If the state accepts the empty string, we found a valid starting position. Record it and keep going, - // since we're looking for the earliest one to occur within bounds. - if (_nullabilityArray[state.DfaStateId] != 0 && - TNullabilityHandler.IsNullableAt(this, in state, positionId)) - { - lastStart = pos; - } + // Loop backwards through each character in the input, transitioning from state to state for each. + bool result = true; + while (true) + { + int positionId = TInputReader.GetPositionId(this, input, pos - 1); - // If we are past the start threshold or if the state is a dead end, bail; we should have already - // found a valid starting location. - if (pos <= startThreshold || state.DfaStateId == _deadStateId) - { - Debug.Assert(lastStart != -1); - return true; - } + // If the state accepts the empty string, we found a valid starting position. Record it and keep going, + // since we're looking for the earliest one to occur within bounds. + if (_nullabilityArray[state.DfaStateId] != 0 && + TNullabilityHandler.IsNullableAt(this, in state, positionId)) + { + lastStart = pos; + } - // Try to transition with the next character, the one before the current position. - if (!DfaStateHandler.TryTakeTransition(this, ref state.DfaStateId, positionId)) - { - // Return false to indicate the search didn't finish. - return false; - } + // If we are past the start threshold or if the state is a dead end, bail; we should have already + // found a valid starting location. + if (pos <= startThreshold || state.DfaStateId == _deadStateId) + { + Debug.Assert(lastStart != -1); + break; + } - // Since we successfully transitioned, update our current index to match the fact that we consumed the previous character in the input. - pos--; + // Try to transition with the next character, the one before the current position. + if (!DfaStateHandler.TryTakeTransition(this, ref state.DfaStateId, positionId)) + { + // Return false to indicate the search didn't finish. + result = false; + break; } + + // Since we successfully transitioned, update our current index to match the fact that we consumed the previous character in the input. + pos--; } - finally - { - // Write back the local copies of the ref values. - stateRef = state; - i = pos; - } + + // Write back the local copies of the ref values. + stateRef = state; + i = pos; + + return result; } private bool FindStartPositionDeltasNFA(ReadOnlySpan input, ref int i, int startThreshold, ref CurrentState state, ref int lastStart) @@ -969,44 +954,44 @@ private bool FindStartPositionDeltasNFA(ReadO { // To avoid frequent reads/writes to ref values, make and operate on local copies, which we then copy back once before returning. int pos = i; - try - { - // Loop backwards through each character in the input, transitioning from state to state for each. - while (true) - { - int positionId = TInputReader.GetPositionId(this, input, pos - 1); - // If the state accepts the empty string, we found a valid starting position. Record it and keep going, - // since we're looking for the earliest one to occur within bounds. - if (TNullabilityHandler.IsNullableAt(this, in state, positionId)) - { - lastStart = pos; - } + // Loop backwards through each character in the input, transitioning from state to state for each. + bool result = true; + while (true) + { + int positionId = TInputReader.GetPositionId(this, input, pos - 1); - // If we are past the start threshold or if the state is a dead end, bail; we should have already - // found a valid starting location. - if (pos <= startThreshold || state.DfaStateId == _deadStateId) - { - Debug.Assert(lastStart != -1); - return true; - } + // If the state accepts the empty string, we found a valid starting position. Record it and keep going, + // since we're looking for the earliest one to occur within bounds. + if (TNullabilityHandler.IsNullableAt(this, in state, positionId)) + { + lastStart = pos; + } - // Try to transition with the next character, the one before the current position. - if (!NfaStateHandler.TryTakeTransition(this, ref state, positionId)) - { - // Return false to indicate the search didn't finish. - return false; - } + // If we are past the start threshold or if the state is a dead end, bail; we should have already + // found a valid starting location. + if (pos <= startThreshold || state.DfaStateId == _deadStateId) + { + Debug.Assert(lastStart != -1); + break; + } - // Since we successfully transitioned, update our current index to match the fact that we consumed the previous character in the input. - pos--; + // Try to transition with the next character, the one before the current position. + if (!NfaStateHandler.TryTakeTransition(this, ref state, positionId)) + { + // Return false to indicate the search didn't finish. + result = false; + break; } + + // Since we successfully transitioned, update our current index to match the fact that we consumed the previous character in the input. + pos--; } - finally - { - // Write back the local copies of the ref values. - i = pos; - } + + // Write back the local copies of the ref values. + i = pos; + + return result; } @@ -1280,7 +1265,7 @@ public static bool IsNullableFor(SymbolicRegexMatcher matcher, in CurrentS /// Take the transition to the next DFA state. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool TryTakeTransition(SymbolicRegexMatcher matcher, ref int dfaStateId, int mintermId) + public static bool TryTakeTransition(SymbolicRegexMatcher matcher, ref int dfaStateId, int mintermId, long timeoutOccursAt = 0) { Debug.Assert(dfaStateId > 0, $"Expected non-zero {nameof(dfaStateId)}."); @@ -1297,7 +1282,7 @@ public static bool TryTakeTransition(SymbolicRegexMatcher matcher, ref int return true; } - if (matcher.TryCreateNewTransition(matcher.GetState(dfaStateId), mintermId, dfaOffset, checkThreshold: true, timeoutOccursAt: 0, out MatchingState? nextState)) + if (matcher.TryCreateNewTransition(matcher.GetState(dfaStateId), mintermId, dfaOffset, checkThreshold: true, timeoutOccursAt, out MatchingState? nextState)) { // We were able to create a new DFA transition to some state. Move to it and // return that we're still operating as a DFA and can keep going. @@ -1308,35 +1293,6 @@ public static bool TryTakeTransition(SymbolicRegexMatcher matcher, ref int return false; } - /// Transition function that only considers DFA state id - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static bool TryTakeDFATransition(SymbolicRegexMatcher matcher, ref int state, int mintermId, long timeoutOccursAt) - { - Debug.Assert(state > 0, $"Expected {nameof(state)} {state} > 0"); - - // Use the mintermId for the character being read to look up which state to transition to. - // If that state has already been materialized, move to it, and we're done. If that state - // hasn't been materialized, try to create it; if we can, move to it, and we're done. - int nextStateId = matcher._dfaDelta[matcher.DeltaOffset(state, mintermId)]; - if (nextStateId > 0) - { - // There was an existing DFA transition to some state. Move to it and - // return that we're still operating as a DFA and can keep going. - state = nextStateId; - return true; - } - - if (matcher.TryCreateNewTransition(matcher.GetState(state), mintermId, matcher.DeltaOffset(state, mintermId), checkThreshold: true, timeoutOccursAt, out MatchingState? nextState)) - { - // We were able to create a new DFA transition to some state. Move to it and - // return that we're still operating as a DFA and can keep going. - state = nextState.Id; - return true; - } - - return false; - } - /// /// Gets context independent state information: /// - whether this is an initial state