From 23a822eeea5254ddf7891155a80cccdc21570353 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20M=C3=BCller?= Date: Sun, 29 Mar 2026 16:00:53 +0200 Subject: [PATCH 1/8] ``: Optimize searches for patterns with initial branching --- stl/inc/regex | 114 +++++++++++++++--- .../GH_005204_regex_collating_ranges/test.cpp | 19 +++ .../std/tests/VSO_0000000_regex_use/test.cpp | 56 +++++++++ 3 files changed, 175 insertions(+), 14 deletions(-) diff --git a/stl/inc/regex b/stl/inc/regex index a23aac9f8e9..c57bc332379 100644 --- a/stl/inc/regex +++ b/stl/inc/regex @@ -4861,6 +4861,23 @@ void _Matcher3<_Elem, _RxTraits, _It, _Alloc>::_Copy_captures(match_results<_Bid _Matches._Null().second = _End; } +template +void _Advance_at_most(_FwdIt& _Pos, const _FwdIt& _Last, _Int _Amount) { + if constexpr (_Is_ranges_random_iter_v<_FwdIt>) { + auto _Shift_amount = _Last - _Pos; + if (_Shift_amount > _Amount) { + _Shift_amount = static_cast(_Amount); + } + + _Pos += _Shift_amount; + } else { + while (_Amount > 0 && _Pos != _Last) { + ++_Pos; + --_Amount; + } + } +} + template _It _Matcher3<_Elem, _RxTraits, _It, _Alloc>::_Skip( _It _First, const _It _Last, const _Node_base* const _Node_arg, const unsigned int _Recursion_depth) { @@ -4869,7 +4886,8 @@ _It _Matcher3<_Elem, _RxTraits, _It, _Alloc>::_Skip( static constexpr char _Line_terminators_char[] = {static_cast(_Meta_cr), static_cast(_Meta_nl)}; static constexpr wchar_t _Line_terminators_wchar_t[] = {static_cast(_Meta_cr), static_cast(_Meta_nl), static_cast(_Meta_ls), static_cast(_Meta_ps)}; - constexpr unsigned int _Max_recursion_depth = 50U; + static constexpr unsigned int _Max_recursion_depth = 50U; + static constexpr short _Max_lookahead = 512; const _Node_base* _Nx = _Node_arg ? _Node_arg : _Start; while (_First != _Last && _Nx) { // check current node @@ -4917,9 +4935,21 @@ _It _Matcher3<_Elem, _RxTraits, _It, _Alloc>::_Skip( case _N_str: { // check for string match - const auto _Node = static_cast*>(_Nx); - const auto _Str = _Node->_Data._Str(); - return _STD _Search_translate_left(_First, _Last, _Str, _Str + _Node->_Data._Size(), _Traits, _Sflags); + const auto _Node = static_cast*>(_Nx); + const auto _Str = _Node->_Data._Str(); + const unsigned int _Size = _Node->_Data._Size(); + + auto _Shifted_last = _Last; + if (_Last != _End && _Size > 1U) { // have to continue search beyond the search window + _STD _Advance_at_most(_Shifted_last, _End, static_cast(_Size - 1U)); + } + + auto _Result = _STD _Search_translate_left(_First, _Shifted_last, _Str, _Str + _Size, _Traits, _Sflags); + if (_Shifted_last == _Result) { // correct for search window shift + return _Last; + } else { + return _Result; + } } case _N_class: @@ -4927,7 +4957,7 @@ _It _Matcher3<_Elem, _RxTraits, _It, _Alloc>::_Skip( const auto _Node = static_cast*>(_Nx); for (; _First != _Last; ++_First) { // look for starting match - if (_Do_class(_Node, _First) != _First) { + if (_Do_class(_Node, _First) != _First) { // may read beyond search window return _First; } } @@ -4952,27 +4982,83 @@ _It _Matcher3<_Elem, _RxTraits, _It, _Alloc>::_Skip( case _N_if: { - // GH-5452: If this node has two or more branches, - // examining all these branches has quadratic worst-case complexity. - // Thus, we only continue if this node has a single branch only. + const auto _Node = static_cast(_Nx); + // TRANSITION, ABI: After GH-5539, the parser no longer generates single-branch _N_if nodes. - // But we have to retain this special handling to avoid performance regression + // But we retain this special handling to avoid some performance regression // when an old parser gets mixed with a new matcher. - const auto _Node = static_cast(_Nx); + if (!_Node->_Child) { + break; + } - if (_Node->_Child) { + if (_Recursion_depth >= _Max_recursion_depth) { return _First; } - break; + + // GH-5452: If this node has two or more branches, + // examining all alternatives in a disjunction + // until the end of the input string + // could result in quadratic worst-case complexity. + // For this reason, we split the input string + // into search windows with a constant maximum length. + // This ensures that this heuristic has linear time complexity, + // because this bounds the number of characters that were read + // beyond the finally determined skip position by a constant. + + for (;;) { + _It _Lookahead_last = _First; + _STD _Advance_at_most(_Lookahead_last, _Last, _Max_lookahead); + + _It _Alt_last = _Lookahead_last; + + for (const _Node_if* _Alternative = _Node; _Alternative && _First != _Alt_last; + _Alternative = _Alternative->_Child) { + _Alt_last = _Skip(_First, _Alt_last, _Alternative->_Next, _Recursion_depth + 1U); + } + + _First = _Alt_last; + if (_First != _Lookahead_last || _First == _Last) { + break; + } + } + + return _First; } case _N_rep: { const auto _Node = static_cast(_Nx); - if (_Node->_Min == 0) { + + if (_Node->_Min > 0) { + break; + } + + if (_Recursion_depth >= _Max_recursion_depth) { return _First; } - break; + + // As in GH-5452, examining the cases + // with no repetition and at least one repetition + // until the end of the input string + // could result in quadratic worst-case complexity. + // For this reason, we split the input string + // into search windows with a constant maximum length. + // This ensures that this heuristic has linear time complexity, + // because this bounds the number of characters that were read + // beyond the finally determined skip position by a constant. + + for (;;) { + _It _Lookahead_last = _First; + _STD _Advance_at_most(_Lookahead_last, _Last, _Max_lookahead); + _First = _Skip(_First, _Skip(_First, _Lookahead_last, _Node->_Next, _Recursion_depth + 1U), + _Node->_End_rep->_Next, _Recursion_depth + 1U); + + if (_First != _Lookahead_last || _First == _Last) { + break; + } + } + + return _First; } case _N_assert: diff --git a/tests/std/tests/GH_005204_regex_collating_ranges/test.cpp b/tests/std/tests/GH_005204_regex_collating_ranges/test.cpp index 687812ce480..91cd833c3f5 100644 --- a/tests/std/tests/GH_005204_regex_collating_ranges/test.cpp +++ b/tests/std/tests/GH_005204_regex_collating_ranges/test.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception #include +#include #include #include #include @@ -656,11 +657,29 @@ void test_gh_5437() { #endif // !defined(SKIP_COLLATE_TESTS) } +void test_gh_6191() { + // GH-6191: Optimize searches for patterns with initial branching + // Check that collating elements are handled correctly at search window boundaries. + const gh_994_regex re("[[.dzs.]]|abc"); + smatch sm; + for (size_t count = 510; count < 516; ++count) { + const string prefix(count, 'h'); + const string dzs_before_abc = prefix + "dzshhhhhabchh"; + assert(regex_search(dzs_before_abc, sm, re)); + assert(string(sm[0].first, sm[0].second) == "dzs"); + + const string abc_before_dzs = prefix + "abchhhhhdzshh"; + assert(regex_search(abc_before_dzs, sm, re)); + assert(string(sm[0].first, sm[0].second) == "abc"); + } +} + int main() { test_collating_ranges_german(); test_gh_994(); test_gh_5435(); test_gh_5437(); + test_gh_6191(); return g_regexTester.result(); } diff --git a/tests/std/tests/VSO_0000000_regex_use/test.cpp b/tests/std/tests/VSO_0000000_regex_use/test.cpp index c4ea150e960..0209fe3c989 100644 --- a/tests/std/tests/VSO_0000000_regex_use/test.cpp +++ b/tests/std/tests/VSO_0000000_regex_use/test.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -2507,6 +2508,60 @@ void test_gh_6181() { } } +void test_gh_6191() { + // GH-6191: Optimize searches for patterns with initial branching + // We must check that we handle matches near search window boundaries correctly. + + { + const test_regex test_alt_re(&g_regexTester, "abcdef|uvwxyz"); + test_alt_re.should_search_match("abcdef", "abcdef"); + test_alt_re.should_search_match("uvwxyz", "uvwxyz"); + test_alt_re.should_search_match("hhabcdef", "abcdef"); + test_alt_re.should_search_match("hhuvwxyz", "uvwxyz"); + test_alt_re.should_search_match("hhhhhuvwxyzhhhhhabcdefhhhhh", "uvwxyz"); + for (size_t count = 510; count < 516; ++count) { + const string prefix(count, 'h'); + test_alt_re.should_search_match(prefix + "abcdef", "abcdef"); + test_alt_re.should_search_match(prefix + "uvwxyz", "uvwxyz"); + test_alt_re.should_search_match(prefix + "abcdefhhhhhhhuvwxyz", "abcdef"); + test_alt_re.should_search_match(prefix + "uvwxyzhhhhhhhabcdef", "uvwxyz"); + test_alt_re.should_search_match(prefix + "abcdefhhhhhhhuvwxyzhhhh", "abcdef"); + test_alt_re.should_search_match(prefix + "uvwxyzhhhhhhhabcdefhhhh", "uvwxyz"); + } + } + + { + const test_regex optional_prefix_re(&g_regexTester, "(abc)?def"); + optional_prefix_re.should_search_match("abcdef", "abcdef"); + optional_prefix_re.should_search_match("def", "def"); + optional_prefix_re.should_search_match("hhabcdef", "abcdef"); + optional_prefix_re.should_search_match("hhdef", "def"); + optional_prefix_re.should_search_match("hhhhabcdefhhhhdefhhh", "abcdef"); + optional_prefix_re.should_search_match("hhhdefhhhhabcdefhhh", "def"); + for (size_t count = 510; count < 516; ++count) { + const string prefix(count, 'h'); + optional_prefix_re.should_search_match(prefix + "abcdef", "abcdef"); + optional_prefix_re.should_search_match(prefix + "def", "def"); + optional_prefix_re.should_search_match(prefix + "abcdefhhhhhhhdef", "abcdef"); + optional_prefix_re.should_search_match(prefix + "defhhhhhhhabcdef", "def"); + } + } + + // test bidirectional iterators + { + const regex alt_re("abcdef|uvwxyz"); + const string suffix = "abcdefhhhhhhhuvwxyz"; + for (size_t count = 510; count < 516; ++count) { + list input(count, 'h'); + input.insert(input.end(), suffix.begin(), suffix.end()); + + match_results::iterator> results; + assert(regex_search(input.begin(), input.end(), results, alt_re)); + assert(string(results[0].first, results[0].second) == "abcdef"); + } + } +} + int main() { test_dev10_449367_case_insensitivity_should_work(); test_dev11_462743_regex_collate_should_not_disable_regex_icase(); @@ -2572,6 +2627,7 @@ int main() { test_gh_6118(); test_gh_6147(); test_gh_6181(); + test_gh_6191(); return g_regexTester.result(); } From 39e30a956c9bb9c64143510e9ff0569f39966cbc Mon Sep 17 00:00:00 2001 From: "Stephan T. Lavavej" Date: Mon, 30 Mar 2026 09:37:44 -0700 Subject: [PATCH 2/8] Rewrap comments. --- stl/inc/regex | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/stl/inc/regex b/stl/inc/regex index c57bc332379..3a2461f880c 100644 --- a/stl/inc/regex +++ b/stl/inc/regex @@ -4995,15 +4995,11 @@ _It _Matcher3<_Elem, _RxTraits, _It, _Alloc>::_Skip( return _First; } - // GH-5452: If this node has two or more branches, - // examining all alternatives in a disjunction - // until the end of the input string - // could result in quadratic worst-case complexity. - // For this reason, we split the input string - // into search windows with a constant maximum length. - // This ensures that this heuristic has linear time complexity, - // because this bounds the number of characters that were read - // beyond the finally determined skip position by a constant. + // GH-5452: If this node has two or more branches, examining all alternatives in a disjunction + // until the end of the input string could result in quadratic worst-case complexity. + // For this reason, we split the input string into search windows with a constant maximum length. + // This ensures that this heuristic has linear time complexity, because this bounds the number + // of characters that were read beyond the finally determined skip position by a constant. for (;;) { _It _Lookahead_last = _First; @@ -5037,15 +5033,11 @@ _It _Matcher3<_Elem, _RxTraits, _It, _Alloc>::_Skip( return _First; } - // As in GH-5452, examining the cases - // with no repetition and at least one repetition - // until the end of the input string - // could result in quadratic worst-case complexity. - // For this reason, we split the input string - // into search windows with a constant maximum length. - // This ensures that this heuristic has linear time complexity, - // because this bounds the number of characters that were read - // beyond the finally determined skip position by a constant. + // As in GH-5452, examining the cases with no repetition and at least one repetition + // until the end of the input string could result in quadratic worst-case complexity. + // For this reason, we split the input string into search windows with a constant maximum length. + // This ensures that this heuristic has linear time complexity, because this bounds the number + // of characters that were read beyond the finally determined skip position by a constant. for (;;) { _It _Lookahead_last = _First; From 9303954aacb9d5a9025b18b00fbc36941452bbc2 Mon Sep 17 00:00:00 2001 From: "Stephan T. Lavavej" Date: Mon, 30 Mar 2026 09:41:23 -0700 Subject: [PATCH 3/8] Scalars should be plain constexpr. --- stl/inc/regex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stl/inc/regex b/stl/inc/regex index 3a2461f880c..75f8b724d76 100644 --- a/stl/inc/regex +++ b/stl/inc/regex @@ -4886,8 +4886,8 @@ _It _Matcher3<_Elem, _RxTraits, _It, _Alloc>::_Skip( static constexpr char _Line_terminators_char[] = {static_cast(_Meta_cr), static_cast(_Meta_nl)}; static constexpr wchar_t _Line_terminators_wchar_t[] = {static_cast(_Meta_cr), static_cast(_Meta_nl), static_cast(_Meta_ls), static_cast(_Meta_ps)}; - static constexpr unsigned int _Max_recursion_depth = 50U; - static constexpr short _Max_lookahead = 512; + constexpr unsigned int _Max_recursion_depth = 50U; + constexpr short _Max_lookahead = 512; const _Node_base* _Nx = _Node_arg ? _Node_arg : _Start; while (_First != _Last && _Nx) { // check current node From 4f41a21ff82b4137b2da4d6fb4c09c5443f4eec2 Mon Sep 17 00:00:00 2001 From: "Stephan T. Lavavej" Date: Mon, 30 Mar 2026 14:29:46 -0700 Subject: [PATCH 4/8] Extract nested `_Skip` to an `_Intermediate` result. --- stl/inc/regex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stl/inc/regex b/stl/inc/regex index 75f8b724d76..64e91e46e7a 100644 --- a/stl/inc/regex +++ b/stl/inc/regex @@ -5042,8 +5042,8 @@ _It _Matcher3<_Elem, _RxTraits, _It, _Alloc>::_Skip( for (;;) { _It _Lookahead_last = _First; _STD _Advance_at_most(_Lookahead_last, _Last, _Max_lookahead); - _First = _Skip(_First, _Skip(_First, _Lookahead_last, _Node->_Next, _Recursion_depth + 1U), - _Node->_End_rep->_Next, _Recursion_depth + 1U); + const _It _Intermediate = _Skip(_First, _Lookahead_last, _Node->_Next, _Recursion_depth + 1U); + _First = _Skip(_First, _Intermediate, _Node->_End_rep->_Next, _Recursion_depth + 1U); if (_First != _Lookahead_last || _First == _Last) { break; From baa31e5fa9caf2cfde391c4ae257794679e71bdb Mon Sep 17 00:00:00 2001 From: "Stephan T. Lavavej" Date: Mon, 30 Mar 2026 14:47:18 -0700 Subject: [PATCH 5/8] Include `` for `size_t`. --- tests/std/tests/GH_005204_regex_collating_ranges/test.cpp | 1 + tests/std/tests/VSO_0000000_regex_use/test.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/tests/std/tests/GH_005204_regex_collating_ranges/test.cpp b/tests/std/tests/GH_005204_regex_collating_ranges/test.cpp index 91cd833c3f5..9efe06ee1fc 100644 --- a/tests/std/tests/GH_005204_regex_collating_ranges/test.cpp +++ b/tests/std/tests/GH_005204_regex_collating_ranges/test.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include diff --git a/tests/std/tests/VSO_0000000_regex_use/test.cpp b/tests/std/tests/VSO_0000000_regex_use/test.cpp index 0209fe3c989..5a1790182c1 100644 --- a/tests/std/tests/VSO_0000000_regex_use/test.cpp +++ b/tests/std/tests/VSO_0000000_regex_use/test.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception #include +#include #include #include #include From 7648c4ea63b473f9b0bcae533277056e3bcb20a2 Mon Sep 17 00:00:00 2001 From: "Stephan T. Lavavej" Date: Mon, 30 Mar 2026 14:47:44 -0700 Subject: [PATCH 6/8] `count` => `prefix_size`, `input_size` for clarity and to avoid shadowing. --- .../tests/GH_005204_regex_collating_ranges/test.cpp | 4 ++-- tests/std/tests/VSO_0000000_regex_use/test.cpp | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/std/tests/GH_005204_regex_collating_ranges/test.cpp b/tests/std/tests/GH_005204_regex_collating_ranges/test.cpp index 9efe06ee1fc..25c39fe1584 100644 --- a/tests/std/tests/GH_005204_regex_collating_ranges/test.cpp +++ b/tests/std/tests/GH_005204_regex_collating_ranges/test.cpp @@ -663,8 +663,8 @@ void test_gh_6191() { // Check that collating elements are handled correctly at search window boundaries. const gh_994_regex re("[[.dzs.]]|abc"); smatch sm; - for (size_t count = 510; count < 516; ++count) { - const string prefix(count, 'h'); + for (size_t prefix_size = 510; prefix_size < 516; ++prefix_size) { + const string prefix(prefix_size, 'h'); const string dzs_before_abc = prefix + "dzshhhhhabchh"; assert(regex_search(dzs_before_abc, sm, re)); assert(string(sm[0].first, sm[0].second) == "dzs"); diff --git a/tests/std/tests/VSO_0000000_regex_use/test.cpp b/tests/std/tests/VSO_0000000_regex_use/test.cpp index 5a1790182c1..f160c1ea2a8 100644 --- a/tests/std/tests/VSO_0000000_regex_use/test.cpp +++ b/tests/std/tests/VSO_0000000_regex_use/test.cpp @@ -2520,8 +2520,8 @@ void test_gh_6191() { test_alt_re.should_search_match("hhabcdef", "abcdef"); test_alt_re.should_search_match("hhuvwxyz", "uvwxyz"); test_alt_re.should_search_match("hhhhhuvwxyzhhhhhabcdefhhhhh", "uvwxyz"); - for (size_t count = 510; count < 516; ++count) { - const string prefix(count, 'h'); + for (size_t prefix_size = 510; prefix_size < 516; ++prefix_size) { + const string prefix(prefix_size, 'h'); test_alt_re.should_search_match(prefix + "abcdef", "abcdef"); test_alt_re.should_search_match(prefix + "uvwxyz", "uvwxyz"); test_alt_re.should_search_match(prefix + "abcdefhhhhhhhuvwxyz", "abcdef"); @@ -2539,8 +2539,8 @@ void test_gh_6191() { optional_prefix_re.should_search_match("hhdef", "def"); optional_prefix_re.should_search_match("hhhhabcdefhhhhdefhhh", "abcdef"); optional_prefix_re.should_search_match("hhhdefhhhhabcdefhhh", "def"); - for (size_t count = 510; count < 516; ++count) { - const string prefix(count, 'h'); + for (size_t prefix_size = 510; prefix_size < 516; ++prefix_size) { + const string prefix(prefix_size, 'h'); optional_prefix_re.should_search_match(prefix + "abcdef", "abcdef"); optional_prefix_re.should_search_match(prefix + "def", "def"); optional_prefix_re.should_search_match(prefix + "abcdefhhhhhhhdef", "abcdef"); @@ -2552,8 +2552,8 @@ void test_gh_6191() { { const regex alt_re("abcdef|uvwxyz"); const string suffix = "abcdefhhhhhhhuvwxyz"; - for (size_t count = 510; count < 516; ++count) { - list input(count, 'h'); + for (size_t input_size = 510; input_size < 516; ++input_size) { + list input(input_size, 'h'); input.insert(input.end(), suffix.begin(), suffix.end()); match_results::iterator> results; From 5870830aa669612b1cbc09a74b9341dc46b0268d Mon Sep 17 00:00:00 2001 From: "Stephan T. Lavavej" Date: Mon, 30 Mar 2026 14:56:50 -0700 Subject: [PATCH 7/8] Use `list::const_iterator`. --- tests/std/tests/VSO_0000000_regex_use/test.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/std/tests/VSO_0000000_regex_use/test.cpp b/tests/std/tests/VSO_0000000_regex_use/test.cpp index f160c1ea2a8..d70452104ce 100644 --- a/tests/std/tests/VSO_0000000_regex_use/test.cpp +++ b/tests/std/tests/VSO_0000000_regex_use/test.cpp @@ -2556,8 +2556,8 @@ void test_gh_6191() { list input(input_size, 'h'); input.insert(input.end(), suffix.begin(), suffix.end()); - match_results::iterator> results; - assert(regex_search(input.begin(), input.end(), results, alt_re)); + match_results::const_iterator> results; + assert(regex_search(input.cbegin(), input.cend(), results, alt_re)); assert(string(results[0].first, results[0].second) == "abcdef"); } } From 834b18a055b966cc5fc2c4056fda1e16b25bcd44 Mon Sep 17 00:00:00 2001 From: "Stephan T. Lavavej" Date: Mon, 30 Mar 2026 15:12:11 -0700 Subject: [PATCH 8/8] Rework list loop to avoid allocating a zillion nodes. --- tests/std/tests/VSO_0000000_regex_use/test.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/std/tests/VSO_0000000_regex_use/test.cpp b/tests/std/tests/VSO_0000000_regex_use/test.cpp index d70452104ce..32b5569d5ba 100644 --- a/tests/std/tests/VSO_0000000_regex_use/test.cpp +++ b/tests/std/tests/VSO_0000000_regex_use/test.cpp @@ -2552,10 +2552,11 @@ void test_gh_6191() { { const regex alt_re("abcdef|uvwxyz"); const string suffix = "abcdefhhhhhhhuvwxyz"; - for (size_t input_size = 510; input_size < 516; ++input_size) { - list input(input_size, 'h'); - input.insert(input.end(), suffix.begin(), suffix.end()); + list input(509, 'h'); + input.insert(input.end(), suffix.begin(), suffix.end()); + for (size_t prefixes_to_test = 0; prefixes_to_test < 6; ++prefixes_to_test) { + input.push_front('h'); match_results::const_iterator> results; assert(regex_search(input.cbegin(), input.cend(), results, alt_re)); assert(string(results[0].first, results[0].second) == "abcdef");