re_intuit_start(): fix another utf8 slowdown
authorDavid Mitchell <davem@iabyn.com>
Thu, 16 Jan 2014 16:00:41 +0000 (16:00 +0000)
committerDavid Mitchell <davem@iabyn.com>
Fri, 7 Feb 2014 22:39:36 +0000 (22:39 +0000)
The code that looks for a floating substr after a fixed substr has
already been found, was very slow on long utf8 strings. For example
this used to take an hour or more, and now takes millisecs:

    $s = "ab" x 1_000_000;
    utf8::upgrade($s);
    $s =~ /ab.{1,2}x/;

When calculating the maximum position at which the floating substr could
start, there are two possible upper limits.

First, the absolute max position, ignoring the results of the previous
fixed substr match - this is the end-of-string less a bit (last1);

Second, float_max_offset on from the current origin of the regex (this
is dependent on where the fixed substr previously matched).

To decide which of these two values to use (the smaller), it used to
calculate the distance in chars from the regex origin to last1, and if
this was greater than float_max_offset, it used origin + float_max_offset
(in chars) instead.

This distance calculation involved doing a utf8 length calculation on the
majority of the string, which for long strings was a big slowdown.

Fix this by instead always using HOP3(origin + float_max_offset), but
using last1 as the upper HOP limit rather than strend, so it's always
limited to <= last1.

If L is number of chars that had to be hopped over for the distance
calculation (which could be most of the string), and if M is the
chars hopped for origin +  float_max_offset (typically either small or
infinite), then we:

    previously hopped: (M>=L ? L : L+M) chars
    now hop:           min(L,M) chars; or if M is infinite, hop 0 chars

Which is always less than or equal to the amount of work done previously,
and is a very big win for long strings with smallish maximum float
offsets.

regexec.c
t/re/pat.t

index 627b16f..5736625 100644 (file)
--- a/regexec.c
+++ b/regexec.c
@@ -129,6 +129,12 @@ static const char* const non_utf8_target_but_utf8_required
 #define HOP3(pos,off,lim) (reginfo->is_utf8_target  ? reghop3((U8*)(pos), off, (U8*)(lim)) : (U8*)(pos + off))
 #define HOP3c(pos,off,lim) ((char*)HOP3(pos,off,lim))
 
+/* like HOP3, but limits the result to <= lim even for the non-utf8 case.
+ * off must be >=0; args should be vars rather than expressions */
+#define HOP3lim(pos,off,lim) (reginfo->is_utf8_target \
+    ? reghop3((U8*)(pos), off, (U8*)(lim)) \
+    : (U8*)((pos + off) > lim ? lim : (pos + off)))
+
 
 #define NEXTCHR_EOS -10 /* nextchr has fallen off the end */
 #define NEXTCHR_IS_EOS (nextchr < 0)
@@ -1004,10 +1010,16 @@ Perl_re_intuit_start(pTHX_
              * <= float_max_offset chars from the regex origin (t).
              * If this value is less than last1, use it instead.
              */
+            assert(t <= last1);
             last = 
-                CHR_DIST((U8*)last1, (U8*)t) > prog->float_max_offset
-                    ? HOP3c(t, prog->float_max_offset, strend)
-                    : last1;
+                /* this condition handles the offset==infinity case, and
+                 * is a short-cut otherwise. Although it's comparing a
+                 * byte offset to a char length, it does so in a safe way,
+                 * meaning it errs towards doing the accurate HOP3 rather
+                 * than just using last1 */
+                (last1 - t) < prog->float_max_offset
+                    ? last1
+                    : (char*)HOP3lim(t, prog->float_max_offset, last1);
 
            s = HOP3c(t, prog->float_min_offset, strend);
            if (s < other_last)
index a052ee7..9296808 100644 (file)
@@ -20,7 +20,7 @@ BEGIN {
     require './test.pl';
 }
 
-plan tests => 714;  # Update this when adding/deleting tests.
+plan tests => 715;  # Update this when adding/deleting tests.
 
 run_tests() unless caller;
 
@@ -1525,6 +1525,9 @@ EOP
 
         $s=~ /^a{1,2}x/ for  1..10_000;
         pass("RT#120692 a{1,2} mustn't run slowly");
+
+        $s=~ /ab.{1,2}x/;
+        pass("RT#120692 ab.{1,2} mustn't run slowly");
     }
 
     # These are based on looking at the code in regcomp.c