Commit e13fc553b6 for perl

commit e13fc553b63ae8e3219884ab3b7252215927be2e
Author: David Mitchell <davem@iabyn.nospamdeletethisbit.com>
Date:   Sat Sep 19 06:19:50 2026 +0100

    regex: make super-linear cache per quantifier

    Before this commit there was a single super-linear cache (SLC) per regex
    execution. Consider for example a pattern such as the following

        /(...)*(...)*(...)*/

    which has three SLC-compatible quantifiers. When matched against a 1
    Mbyte string, the initial countdown would be set to 3E6, then after that
    many WHILEM nodes had been executed, a single cache containing 3E6 bits
    would be allocated.

    Following this commit, there is a separate 1E6 countdown per WHILEM
    node, then an individual 1E6 bit cache is allocated per node.

    For a pattern where not all the quantifiers go super-linear, this has
    the twin advantages that the SLC kicks in quicker (resulting in less
    wasted iterations) and the amount of memory allocated is smaller.

    As a concrete example, this match:

               "aa;bbbbbbbbbbbbbbbbbbbb;cc"
            =~ /^(aa?)*;(bb?)*bbbbbbbbbbbbbbbbbb;(cc?)*$/;

    only goes super-linear in the second of the three '*' quantifiers.
    Previously, running this pattern caused 122 WHILEM executions and
    allocated 3*26 bits of cache. Now it's 71 and 1*26.

    For longer strings, time and memory savings can become significant.

    See the changes to perlreguts.pod contained in this commit for the
    details of how the new system works.

    Some of the commits prior to this one simplified things, such as
    guaranteeing that a cache never needs to be re-allocated to a larger
    size. In turn, this has made it possible to make some of the code in
    this commit simpler or more efficient. For example ST.slc_bytep is now a
    direct pointer to a byte in the cache, whereas previously
    ST.cache_offset was an offset, which required more calculations on the
    fly when setting a cache bit in CacheSayNO.

    This commit also adds a depth field to the regexp_internal struct,
    analogous to the CvDEPTH() field of a CV. This makes makes it easier to
    detect whether a regex is being executed recursively; for example:

        sub f { ....; /....(?{ f($i-1) }) .../ }

    This might turn out to be useful to simplify other things in the engine
    (like capture offsets possibly), which currently have messy workarounds.

diff --git a/pod/perlreguts.pod b/pod/perlreguts.pod
index 4d9dab6bdd..1e584d35e0 100644
--- a/pod/perlreguts.pod
+++ b/pod/perlreguts.pod
@@ -1217,18 +1217,18 @@ don't need to record the min and max values any more.

 With this simplification, we just need to record a single bit of
 information ("failed already") for every (quantifier-id, current string
-position) tuple. We can achieve this by allocating a single bit array
-whose size is equal to the string length multiplied by the number of
-candidate quantifiers in the pattern. So for example when matching a 1
-Mbyte string against a pattern like C</(...)*(...)*/> which has two
-quantifiers, 2 million bits must be allocated.
+position) tuple. We can achieve this by potentially allocating, for each
+candidate quantifier in the pattern, a bit array whose size is equal to
+the string length. So for example when matching a 1 Mbyte string against a
+pattern like C</(...)*(...)*/> which has two quantifiers, two
+1-million-bit arrays might be allocated.

 To avoid a potentially large malloc() for every match that has been marked
-as suitable for a SLC, a countdown is initiated the first time a candidate
-C<WHILEM> node is reached; only after (string length) multiplied by
-(number of participating C<WHILEM> nodes) iterations is the cache actually
-allocated and initialised. This crude heuristic is an indication that the
-match has gone super-linear.
+as suitable for a SLC, a per-node countdown is initiated the first time a
+candidate C<WHILEM> node is reached; only when the number of iterations of
+that particular node is equal to the string length is a cache for that
+node actually allocated and initialised. This crude heuristic is an
+indication that the match has gone super-linear.

 =item *

@@ -1379,37 +1379,42 @@ per regex.

 =back

+=head4 The super-linear cache at run-time
+
 In a compiled regex, a node id in the range 1-15 is stored in the
 C<FLAGS()> field of each participating C<WHILEM> node (a value of 0
 indicates that the node isn't suitable for the SLC). The field
 C<slc_whilem_seen> in the C<regexp_internal> structure indicates the total
-number of such  C<WHILEM> nodes.
-
-The runtime state of the SLC is mainly stored in various fields of the
-C<reginfo> struct, which is initialised at the start of a match.
-
-A pointer to the cache is stored in C<< reginfo->info_aux.poscache >>,
-which will be freed when matching ends (the aux structure is guaranteed to
-be freed even on abnormal termination).
-
-If zero, C<< reginfo->poscache_maxiter >> indicates that the SLC countdown
-has not yet been triggered (i.e. no candidate C<WHILEM> node has been
-executed yet). Otherwise, its (positive) value is used for two different
-purposes: what value to start an initial (or reset) countdown from; and
-the size to C<alloc()> the cache, in bits. Currently these values are the
-same, but in principle they needn't be.
-
-C<< reginfo->poscache_iter >> only has meaning if C<poscache_maxiter> is
-non-zero. In that case, it represents a countdown initialised from
-C<poscache_maxiter>. If it reaches 1, the cache is malloced if necessary,
-and then zeroed. When it reaches 0, the cache is used.
-
-The C<CACHEsayNO> macro is used at runtime in various places as a
-replacement for C<sayNO> to set a fail bit in the cache while popping the
-current state. The C<ST.cache_offset> and C<ST.cache_mask> fields are set
-by the current C<WHILEM> to the address in the cache of the byte and bit
-corresponding to the current match state. These are used by C<CACHEsayNO>
-to mark the cache during a subsequent unwinding.
+number of such C<WHILEM> nodes.
+
+The runtime state is mainly contained in an array of (countdown, bitmap
+pointer) pairs, the size of which is equal to C<slc_whilem_seen>. This
+array is allocated the first time a regex starts to be executed, and is
+kept for future matches, pointed to from the C<slc> field in the
+C<regexp_internal> structure. It is not copied when cloning a thread; it
+relies instead on the first match run in the new thread to allocate it. In
+the rare event of a regex called recursively, a new array is allocated and
+freed after each execution of the pattern.
+
+C<< reginfo->info_aux->slc >> is used to point to the currently running
+array: either the permanent one, or a temporary recursive one.
+
+The first time a participating C<WHILEM> node is executed, the
+C<slc_countdown> field of the array entry indexed by C<FLAGS()> is
+initialised to the string length. Subsequent iterations of that node
+decrement the count. When it reaches zero, a bitmap with a number of bits
+equal to the size of string being matched against is allocated and pointed
+to from the C<slc_bitmap> field of the array entry. All such allocated
+bitmaps are freed at the end of the regex's execution. This is guaranteed
+even on croaking due to the cache array pointer being stored in the
+C<info_aux> structure, which is always processed during clean up.
+
+The C<CACHEsayNO> macro is used in various places as a replacement for
+C<sayNO> to set a fail bit in the cache while popping the current state.
+The C<ST.slc_byte> and C<ST.slc_mask> fields are set by the current
+C<WHILEM> to the address in the cache of the byte and bit corresponding to
+the current match state. These are used by C<CACHEsayNO> to mark the cache
+during a subsequent unwinding.

 The initial cache allocation countdown can be adjusted via
 C<PL_re_superlinear_cache_delay>, which is settable via
diff --git a/regcomp.c b/regcomp.c
index 9b0388396c..0bd4cf8e04 100644
--- a/regcomp.c
+++ b/regcomp.c
@@ -14013,6 +14013,14 @@ Perl_regfree_internal(pTHX_ REGEXP * const rx)
         Safefree(ri->data);
     }

+    if (ri->slc) {
+#ifdef DEBUGGING
+        for (U8 i = 0; i < ri->slc_whilem_seen; i++)
+            assert(!ri->slc[i].slc_bitmap);
+#endif
+        Safefree(ri->slc);
+    }
+
     Safefree(ri);
 }

@@ -14278,6 +14286,8 @@ Perl_regdupe_internal(pTHX_ REGEXP * const rx, CLONE_PARAMS *param)

     reti->name_list_idx = ri->name_list_idx;
     reti->slc_whilem_seen = ri->slc_whilem_seen;
+    reti->slc             = NULL; /* will be alloced if/when needed */
+    reti->depth           = 0;

     SetProgLen(reti, len);

diff --git a/regcomp.h b/regcomp.h
index 252623c02a..594fddac34 100644
--- a/regcomp.h
+++ b/regcomp.h
@@ -83,6 +83,15 @@
 /* Not for production use: */
 #define PERL_ENABLE_EXPERIMENTAL_REGEX_OPTIMISATIONS 0

+/* a cache pointer and countdown for a single super-linear cache compatible
+ * WHILEM node
+ */
+
+struct slc_cache_item {
+    U8 *slc_bitmap;
+    STRLEN slc_countdown;
+};
+
 /*
  * Structure for regexp "program".  This is essentially a linear encoding
  * of a nondeterministic finite-state machine (aka syntax charts or
@@ -132,8 +141,12 @@ typedef struct regexp_internal {
                                    only valid when RXp_PAREN_NAMES(prog) is true,
                                    0 means "no value" like any other index into the
                                    data array.*/
+        U32 depth;              /* 1 = executing; 2+ = recursing */
         U8 slc_whilem_seen;     /* Num of WHILEMs using super-linear cache.
                                    Same type as FLAGS() */
+        struct slc_cache_item *slc; /* permanent array of super-linear cache
+                                       countdown / bitmap pointer pairs*/
+
         regnode program[1];	/* Unwarranted chumminess with compiler. */
 } regexp_internal;

diff --git a/regexec.c b/regexec.c
index dd802ab250..e094c47be5 100644
--- a/regexec.c
+++ b/regexec.c
@@ -1013,8 +1013,6 @@ Perl_re_intuit_start(pTHX_
     reginfo->strend = strend;
     reginfo->is_utf8_pat = cBOOL(RX_UTF8(rx));
     reginfo->intuit = 1;
-    /* not actually used within intuit, but zero for safety anyway */
-    reginfo->poscache_maxiter = 0;
     reginfo->prog = NULL;
     reginfo->sv = NULL;
     reginfo->warned = false;
@@ -3871,13 +3869,14 @@ Perl_regexec_flags(pTHX_ REGEXP * const rx, char *stringarg, char *strend,
     reginfo->is_utf8_pat = cBOOL(RX_UTF8(rx));
     reginfo->warned = false;
     reginfo->sv = sv;
-    reginfo->poscache_maxiter = 0; /* not yet started a countdown */
     /* see how far we have to get to not match where we matched before */
     reginfo->till = stringarg + minend;

     /* zero for safety */
     reginfo->info_aux = NULL;

+    progi->depth++;
+
     if (prog->extflags & RXf_EVAL_SEEN && SvPADTMP(sv)) {
         /* SAVEFREESV, not sv_mortalcopy, as this SV must last until after
            S_cleanup_regmatch_info_aux has executed (registered by
@@ -3928,7 +3927,7 @@ Perl_regexec_flags(pTHX_ REGEXP * const rx, char *stringarg, char *strend,

         reginfo->info_aux->old_regmatch_state = old_regmatch_state;
         reginfo->info_aux->old_regmatch_slab  = old_regmatch_slab;
-        reginfo->info_aux->poscache = NULL;
+        reginfo->info_aux->rexi = progi;

         SAVEDESTRUCTOR_X(S_cleanup_regmatch_info_aux, reginfo->info_aux);

@@ -3938,6 +3937,31 @@ Perl_regexec_flags(pTHX_ REGEXP * const rx, char *stringarg, char *strend,
             reginfo->info_aux_eval = reginfo->info_aux->info_aux_eval = NULL;
     }

+    if (progi->slc_whilem_seen) {
+        /* Allocate array of cache pointers / countdowns for the
+         * super-linear cache; or if already present, zero its countdowns.
+         * Once allocated, it is permanently attached to the
+         * regex_internal struct. Except that on recursion, a new one
+         * is allocated and freed on every run.
+         */
+        struct slc_cache_item *slc = progi->depth > 1 ? NULL : progi->slc;
+        if (slc) {
+#ifdef DEBUGGING
+            for (U8 i = 0; i < progi->slc_whilem_seen; i++)
+                assert(!slc[i].slc_bitmap);
+#endif
+            Zero(slc, progi->slc_whilem_seen, struct slc_cache_item);
+        }
+        else {
+            Newxz(slc, progi->slc_whilem_seen, struct slc_cache_item);
+            if (progi->depth == 1)
+                progi->slc = slc; /* keep for future matches */
+        }
+        reginfo->info_aux->slc = slc;
+    }
+    else
+        reginfo->info_aux->slc = NULL;
+
     if (PL_curpm && (PM_GETRE(PL_curpm) == rx)) {
         /* We have to be careful. If the previous successful match
            was from this regex we don't want a subsequent partially
@@ -6483,7 +6507,7 @@ S_backup_one_WB_but_over_Extend_FO(pTHX_ WB_enum * previous,
 /* we don't use STMT_START/END here because it leads to
    "unreachable code" warnings, which are bogus, but distracting. */
 #define CACHEsayNO \
-    if (ST.cache_mask &&!seen_nonregular) {                            \
+    if (ST.slc_mask && !seen_nonregular) {                             \
         DEBUG_EXECUTE_r({                                              \
             regnode *whilem =                                          \
                 REGNODE_BEFORE(regnext(cur_curlyx->u.curlyx.me));      \
@@ -6492,7 +6516,7 @@ S_backup_one_WB_but_over_Extend_FO(pTHX_ WB_enum * previous,
                 depth, (int)FLAGS(whilem), (int)rexi->slc_whilem_seen, \
                 (UV)(locinput - reginfo->strbeg));                     \
         });                                                            \
-       reginfo->info_aux->poscache[ST.cache_offset] |= ST.cache_mask;  \
+       *ST.slc_byte |= ST.slc_mask;                                    \
     }                                                                  \
     sayNO

@@ -9106,8 +9130,7 @@ NULL
             A = REGNODE_AFTER(cur_curlyx->u.curlyx.me);
             n = ++cur_curlyx->u.curlyx.count; /* how many A's matched */
             ST.save_lastloc = cur_curlyx->u.curlyx.lastloc;
-            ST.cache_offset = 0;
-            ST.cache_mask = 0;
+            ST.slc_mask = 0;

             DEBUG_EXECUTE_r( re_exec_indentf("WHILEM: matched %ld out of %d..%d\n",
                   depth, (long)n, min, max)
@@ -9135,113 +9158,110 @@ NULL

             if (   FLAGS(scan)
                    /* not running a (??{...}) or (?N) sub-pattern */
-                && !cur_eval
-                   /* -1 => disable cache */
-                && PL_re_superlinear_cache_delay != -1)
+                && !cur_eval)
             {
-                /* Super-linear cache processing.
+                /* Super-linear cache (SLC) processing.
                  *
                  * See L<perlreguts/The super-linear cache> for a detailed
                  * background on how this works.
                  *
                  * For WHILEM nodes which can participate in the cache
-                 * (FLAGS() is non-zero), the processing at this point is to
-                 * first initiate a countdown. Then when on subsequent
+                 * (FLAGS() is non-zero), the processing at this point is
+                 * to first initiate a countdown. Then when on subsequent
                  * iterations that reaches zero, the match has likely gone
-                 * super-linear and the cache is allocated and starts to be
+                 * super-linear and the per-WHILEM cache is allocated and
                  * used.
                  */
-#ifdef DEBUGGING
-                if (reginfo->poscache_maxiter) {
-                    DEBUG_OPTIMISE_MORE_r(re_exec_indentf(
-                        "  iter=%" UVuf " maxiter=%" UVuf "\n",
-                    depth,
-                    (UV)reginfo->poscache_iter,
-                    (UV)reginfo->poscache_maxiter)
-                );
+                assert(rexi->slc);
+                assert(reginfo->info_aux->slc);
+                struct slc_cache_item *item =
+                                &reginfo->info_aux->slc[FLAGS(scan)-1];
+
+                if (item->slc_bitmap) {
+                    /* Cache is live */
+                    STRLEN offset;
+                    U8     mask, *bytep;
+                  slc_is_live:
+                    offset = locinput - reginfo->strbeg;
+                    mask   = 1 << (offset % 8);
+                    bytep  = &item->slc_bitmap[offset/8];
+
+                    if (*bytep & mask) {
+                        /* We have already failed at this position */
+                        DEBUG_EXECUTE_r( re_exec_indentf(
+                            "WHILEM[%d/%d]: (cache) already failed at pos %"
+                                                                    UVuf "\n",
+                            depth, (int)FLAGS(scan),
+                            (int)rexi->slc_whilem_seen,
+                            (UV)(locinput - reginfo->strbeg))
+                        );
+                        cur_curlyx->u.curlyx.count--;
+                        sayNO;
+                    }
+
+                    /* Make cache index available to CACHEsayNO */
+                    ST.slc_byte = bytep;
+                    ST.slc_mask = mask;
                 }
-                else {
+                else if (item->slc_countdown) {
+                    /* Cache is not yet live; currently counting down */
                     DEBUG_OPTIMISE_MORE_r(re_exec_indentf(
-                        "  maxiter=0\n", depth)
+                        "  cache countdown=%" UVuf "\n", depth,
+                        (UV)item->slc_countdown)
                     );
-                }
-#endif

-                if (!reginfo->poscache_maxiter) {
-                    /* start the countdown: Postpone detection until we
-                     * know the match is not *that* much linear. */
-                    STRLEN len = reginfo->strend - reginfo->strbeg;
-                    /* number of participating WHILEMs */
-                    U8 n = rexi->slc_whilem_seen;
-                    assert(FLAGS(scan) <= n);
-
-                    /* Only do the calculations and enable the cache if it
-                     * won't overflow. This test is equivalent to:
-                     *    ((len + 1) * n  + 7) <= STRLEN_MAX
-                     */
-                    if (len < (STRLEN_MAX - 7)/n) {
-                        reginfo->poscache_maxiter = (len + 1) * n;
-
-                        if (PL_re_superlinear_cache_delay == 0)
-                            /* use default value  */
-                            reginfo->poscache_iter =
-                                                reginfo->poscache_maxiter;
-                        else if (PL_re_superlinear_cache_delay > 0)
+                    if (!--item->slc_countdown) {
+                        /* countdown finished: alloc and use the cache:
+                         * 1 bit per string byte */
+                        Newxz(item->slc_bitmap,
+                              ((reginfo->strend - reginfo->strbeg + 1) + 7)/8,
+                              U8);
+
+                        DEBUG_EXECUTE_r( re_exec_indentf(
+                            "%sWHILEM[%d/%d]: detected a super-linear match, enabling cache%s...\n",
+                            depth, PL_colors[4],
+                            (int)FLAGS(scan),
+                            (int)rexi->slc_whilem_seen,
+                            PL_colors[5]
+                        ));
+
+                        goto slc_is_live;
+                    }
+                }
+                else {
+                    /* Cache is not yet live; countdown not yet started.
+                     * Initialise the countdown: postpone detection until
+                     * we know that the match is not *that* much
+                     * linear. Note that a degenerate zero-length
+                     * string will have the effect of not starting a
+                     * countdown */
+                    STRLEN count = reginfo->strend - reginfo->strbeg;
+
+                    if (PL_re_superlinear_cache_delay) {
+                        /* Apply countdown modifier */
+                        if (PL_re_superlinear_cache_delay > 0)
                             /* use specified value  */
-                            reginfo->poscache_iter =
-                                                PL_re_superlinear_cache_delay;
+                            count = PL_re_superlinear_cache_delay;
+                        else if (PL_re_superlinear_cache_delay == -1)
+                            /* disable cache processing */
+                            count = 0;
                         else {
-                            /* negative (-1 already checked for above)
-                             * use -N/1E6 scaling factor */
+                             /* use -N/1E6 scaling factor */
                             NV delay =
                                 -(NV)PL_re_superlinear_cache_delay / 1E6
-                                 * (NV)reginfo->poscache_maxiter;
-                            reginfo->poscache_iter =
-                                delay >= (NV)STRLEN_MAX
+                                 * (NV)count;
+                            count = delay >= (NV)STRLEN_MAX
                                     ? STRLEN_MAX
                                     : delay < 1 ? 1 : delay;
                         }
                     }
-                }

-                if (reginfo->poscache_iter == 1) {
-                    reginfo->poscache_iter--;
-                    /* initialise cache */
-                    const STRLEN size = (reginfo->poscache_maxiter + 7)/8;
-                    regmatch_info_aux *const aux = reginfo->info_aux;
-                    assert(!aux->poscache);
-                    Newxz(aux->poscache, size, char);
-
-                    DEBUG_EXECUTE_r( re_exec_indentf(
-      "%sWHILEM: Detected a super-linear match, enabling cache%s...\n",
-                              depth, PL_colors[4], PL_colors[5])
+                    item->slc_countdown = count;
+                    DEBUG_OPTIMISE_MORE_r(re_exec_indentf(
+                        "  cache countdown initialised to %" UVuf "\n",
+                        depth, (UV)count)
                     );
                 }
-
-                if (reginfo->poscache_iter == 0) {
-                    /* have we already failed at this position? */
-                    SSize_t offset, mask;
-
-                    offset  = FLAGS(scan) - 1
-                                +   (locinput - reginfo->strbeg)
-                                  * rexi->slc_whilem_seen;
-                    mask    = 1 << (offset % 8);
-                    offset /= 8;
-                    if (reginfo->info_aux->poscache[offset] & mask) {
-                        DEBUG_EXECUTE_r( re_exec_indentf(
-                            "WHILEM[%d/%d]: (cache) already failed at pos %" UVuf "\n",
-                            depth, (int)FLAGS(scan),
-                            (int)rexi->slc_whilem_seen,
-                            (UV)(locinput - reginfo->strbeg));
-                        );
-                        cur_curlyx->u.curlyx.count--;
-                        sayNO; /* cache records failure */
-                    }
-                    ST.cache_offset = offset;
-                    ST.cache_mask   = mask;
-                }
-                else
-                    reginfo->poscache_iter--;
             }

             /* Prefer B over A for minimal matching. */
@@ -11590,10 +11610,23 @@ S_cleanup_regmatch_info_aux(pTHX_ void *arg)
 {
     regmatch_info_aux *aux = (regmatch_info_aux *) arg;
     regmatch_info_aux_eval *eval_state =  aux->info_aux_eval;
+    regexp_internal *rexi = aux->rexi;
     regmatch_slab *s;

-    if (aux->poscache)
-        Safefree(aux->poscache);
+    assert(rexi->depth > 0);
+    rexi->depth--;
+
+    /* free any allocated super-linear caches */
+    if (aux->slc) {
+        U8 i;
+        for (i = 0; i < rexi->slc_whilem_seen; i++) {
+            Safefree(aux->slc[i].slc_bitmap);
+            aux->slc[i].slc_bitmap = NULL;
+        }
+    }
+    /* free the cache array too if it was used during recursion */
+    if (rexi->depth)
+            Safefree(aux->slc);

     if (eval_state) {

diff --git a/regexp.h b/regexp.h
index ac400e305b..7d57cd9a10 100644
--- a/regexp.h
+++ b/regexp.h
@@ -810,10 +810,11 @@ typedef struct {
  * the regmatch_state stack at the start of execution */

 typedef struct {
+    struct regexp_internal *rexi;
     regmatch_info_aux_eval *info_aux_eval;
     struct regmatch_state *old_regmatch_state; /* saved PL_regmatch_state */
     struct regmatch_slab  *old_regmatch_slab;  /* saved PL_regmatch_slab */
-    char *poscache;	/* S-L cache of fail positions of WHILEMs */
+    struct slc_cache_item *slc; /* current super-liner cache array */
 } regmatch_info_aux;


@@ -839,8 +840,6 @@ typedef struct {
     char *cutpoint;      /* (*COMMIT) position (if any) */
     regmatch_info_aux      *info_aux; /* extra fields that need cleanup */
     regmatch_info_aux_eval *info_aux_eval; /* extra saved state for (?{}) */
-    STRLEN poscache_maxiter; /* how many whilems todo before S-L cache kicks in */
-    STRLEN poscache_iter;    /* current countdown from _maxiter to zero */
     bool intuit;    /* re_intuit_start() is the top-level caller */
     bool is_utf8_pat;    /* regex is utf8 */
     bool is_utf8_target; /* string being matched is utf8 */
@@ -1053,8 +1052,8 @@ typedef struct regmatch_state {
             CHECKPOINT  cp;             /* see note above "struct branchlike" */
             CHECKPOINT  lastcp;         /* see note above "struct branchlike" */
             char        *save_lastloc;  /* previous curlyx.lastloc */
-            I32		cache_offset;
-            I32		cache_mask;
+            U8	        *slc_byte;
+            U8		slc_mask;
             bool        saved_seen_nonregular; /* previous seen_nonregular */
         } whilem;

diff --git a/t/perf/benchmarks b/t/perf/benchmarks
index 30043663a6..b5249ff3f0 100644
--- a/t/perf/benchmarks
+++ b/t/perf/benchmarks
@@ -2687,26 +2687,32 @@

     # XXX more intuit tests needed here

-    # regex superlinear-cache
+    # regex super-linear cache (SLC)

     'regex::slc::immediate' => {
-        desc    => '/slc: immediate',
+        desc    => 'slc: no cache delay',
         setup   => '$_ = "aaaaa"; ${^RE_SUPERLINEAR_CACHE_DELAY}=1',
         code    => '/^(aa?)*[bc]/',
     },

     'regex::slc::default' => {
-        desc    => '/slc: default',
+        desc    => 'slc: default cache delay',
         setup   => '$_ = "aaaaa";',
         code    => '/^(aa?)*[bc]/',
     },

     'regex::slc::never' => {
-        desc    => '/slc: never',
+        desc    => 'slc: never cache',
         setup   => '$_ = "aaaaa"; ${^RE_SUPERLINEAR_CACHE_DELAY}=-1',
         code    => '/^(aa?)*[bc]/',
     },

+    'regex::slc::middle_ony' => {
+        desc    => 'slc: only middle of three quantifiers triggers cache',
+        setup   => '$_ = "a;bbbbbbbbbbbbbbbbbbbb;c";
+                    ${^RE_SUPERLINEAR_CACHE_DELAY}=-200_000',
+        code    => '/^(a|xy)+;(bb?)*bbbbbbbbbb;(c|xy)+$/',
+    },

     # XXX millions more general regex tests needed here
 ];
diff --git a/t/re/pat.t b/t/re/pat.t
index 3299727b34..6b964669ef 100644
--- a/t/re/pat.t
+++ b/t/re/pat.t
@@ -28,7 +28,7 @@ skip_all_without_unicode_tables();
 my $has_locales = locales_enabled('LC_CTYPE');
 my $utf8_locale = find_utf8_ctype_locale();

-plan tests => 1313;  # Update this when adding/deleting tests.
+plan tests => 1314;  # Update this when adding/deleting tests.

 run_tests() unless caller;

@@ -2713,7 +2713,10 @@ SKIP:
             "SLC backref"
         );

+        # multiple quantifiers

+        ok("aa;bbbbbbbbbbbbbbbbbbbb;cc"
+            =~ /^(aa?)*;(bb?)*bbbbbbbbbbbbbbbbbb;(cc?)*$/, "SLC multi");
     }

     {