[PATCH] Use memmem algorithm for strstr

Wilco Dijkstra <[email protected]>
Newsgroups gmane.comp.lib.newlib
Message-ID <DB5PR08MB1030A9C447986450613546D683B60@DB5PR08MB1030.eurprd08.prod.outlook.com>
This patch further improves performance of strstr by using the new memmem
algorithm (http://www.cygwin.com/ml/newlib/2018/msg01151.html). 
The performance gain on English text with randomized needles of average size
8 is 25% on small haystacks and 75% for large haystacks.

Tested against GLIBC testsuite, randomized tests and the GNULIB strstr test
(https://git.savannah.gnu.org/cgit/gnulib.git/tree/tests/test-strstr.c).

--

diff --git a/newlib/libc/string/strstr.c b/newlib/libc/string/strstr.c
index 0256c1aa63a4f945c0b8488236f3d6681119f44f..9d5909a696ecfe7987b436706fbb20ba8eeb105f 100644
--- a/newlib/libc/string/strstr.c
+++ b/newlib/libc/string/strstr.c
@@ -94,9 +94,6 @@ strstr (const char *hs, const char *ne)
 
 # include "str-two-way.h"
 
-/* Number of bits used to index shift table.  */
-#define SHIFT_TABLE_BITS 6
-
 static inline char *
 strstr2 (const unsigned char *hs, const unsigned char *ne)
 {
@@ -107,36 +104,19 @@ strstr2 (const unsigned char *hs, const unsigned char *ne)
   return h1 == h2 ? (char *)hs - 2 : NULL;
 }
 
-static inline char *
-strstr3 (const unsigned char *hs, const unsigned char *ne)
-{
-  uint32_t h1 = (ne[0] << 24) | (ne[1] << 16) | (ne[2] << 8);
-  uint32_t h2 = 0;
-  for (int c = hs[0]; h1 != h2 && c != 0; c = *++hs)
-      h2 = (h2 | c) << 8;
-  return h1 == h2 ? (char *)hs - 3 : NULL;
-}
+#define hash2(p) (((size_t)(p)[0] - ((size_t)(p)[-1] << 3)) % sizeof (shift))
 
-static inline char *
-strstr4 (const unsigned char *hs, const unsigned char *ne)
-{
-  uint32_t h1 = (ne[0] << 24) | (ne[1] << 16) | (ne[2] << 8) | ne[3];
-  uint32_t h2 = 0;
-  for (int c = hs[0]; c != 0 && h1 != h2; c = *++hs)
-    h2 = (h2 << 8) | c;
-  return h1 == h2 ? (char *)hs - 4 : NULL;
-}
-
-/* Extremely fast strstr algorithm with guaranteed linear-time performance.
-   Small needles up to size 4 use a dedicated linear search.  Longer needles
-   up to size 254 use Sunday's Quick-Search algorithm.  Due to its simplicity
-   it has the best average performance of string matching algorithms on almost
-   all inputs.  It uses a bad-character shift table to skip past mismatches.
-   By limiting the needle length to 254, the shift table can be reduced to 8
+/* Fast strstr algorithm with guaranteed linear-time performance.
+   Small needles up to size 2 use a dedicated linear search.  Longer needles
+   up to size 256 use a novel modified Horspool algorithm.  It hashes pairs
+   of characters to quickly skip past mismatches.  The main search loop only
+   exits if the last 2 characters match, avoiding unnecessary calls to memcmp
+   and allowing for a larger skip if there is no match.  A self-adapting
+   filtering check is used to quickly detect mismatches in long needles.
+   By limiting the needle length to 256, the shift table can be reduced to 8
    bits per entry, lowering preprocessing overhead and minimizing cache effects.
-   The limit also implies the worst-case performance is linear.
-   Even larger needles are processed by the linear-time Two-Way algorithm.
-*/
+   The limit also implies worst-case performance is linear.
+   Needles larger than 256 characters use the linear-time Two-Way algorithm.  */
 char *
 strstr (const char *haystack, const char *needle)
 {
@@ -150,10 +130,6 @@ strstr (const char *haystack, const char *needle)
     return (char*)strchr (hs, ne[0]);
   if (ne[2] == '\0')
     return strstr2 (hs, ne);
-  if (ne[3] == '\0')
-    return strstr3 (hs, ne);
-  if (ne[4] == '\0')
-    return strstr4 (hs, ne);
 
   size_t ne_len = strlen (ne);
   size_t hs_len = strnlen (hs, ne_len | 512);
@@ -162,39 +138,59 @@ strstr (const char *haystack, const char *needle)
   if (hs_len < ne_len)
     return NULL;
 
-  /* Use the Quick-Search algorithm for needle lengths less than 255.  */
-  if (__builtin_expect (ne_len < 255, 1))
+  /* Use Two-Way algorithm for very long needles.  */
+  if (__builtin_expect (ne_len > 256, 0))
+    return two_way_long_needle (hs, hs_len, ne, ne_len);
+
+  const unsigned char *end = hs + hs_len - ne_len;
+  uint8_t shift[256];
+  size_t tmp, shift1;
+  size_t m1 = ne_len - 1;
+  size_t offset = 0;
+
+  /* Initialize bad character shift hash table.  */
+  memset (shift, 0, sizeof (shift));
+  for (int i = 1; i < m1; i++)
+    shift[hash2 (ne + i)] = i;
+  shift1 = m1 - shift[hash2 (ne + m1)];
+  shift[hash2 (ne + m1)] = m1;
+
+  while (1)
     {
-      uint8_t shift[1 << SHIFT_TABLE_BITS];
-      const unsigned char *end = hs + hs_len - ne_len;
-
-      /* Initialize bad character shift hash table.  */
-      memset (shift, ne_len + 1, sizeof (shift));
-      for (int i = 0; i < ne_len; i++)
-	shift[ne[i] % sizeof (shift)] = ne_len - i;
+      if (__builtin_expect (hs > end, 0))
+	{
+	  end += strnlen (end + m1 + 1, 2048);
+	  if (hs > end)
+	    return NULL;
+	}
 
+      /* Skip past character pairs not in the needle.  */
       do
 	{
-	  hs--;
-
-	  /* Search by skipping past bad characters.  */
-	  size_t tmp = shift[hs[ne_len] % sizeof (shift)];
-	  for (hs += tmp; hs <= end; hs += tmp)
-	    {
-	      tmp = shift[hs[ne_len] % sizeof (shift)];
-	      if (memcmp (hs, ne, ne_len) == 0)
-		return (char*) hs;
-	    }
-	  if (end[ne_len] == 0)
-	    return NULL;
-	  end += strnlen (end + ne_len, 2048);
+	  hs += m1;
+	  tmp = shift[hash2 (hs)];
 	}
-      while (hs <= end);
+      while (hs <= end && tmp == 0);
 
-      return NULL;
-    }
+      /* If the match is not at the end of the needle, shift to the end
+	 and continue until we match the last 2 characters.  */
+      hs -= tmp;
+      if (tmp < m1)
+	continue;
 
-  /* Use Two-Way algorithm for very long needles.  */
-  return two_way_long_needle (hs, hs_len, ne, ne_len);
+      /* The last 2 characters match.  If the needle is long, check a
+	 fixed number of characters first to quickly filter out mismatches.  */
+      if (m1 <= 15 || memcmp (hs + offset, ne + offset, sizeof (long)) == 0)
+	{
+	  if (memcmp (hs, ne, m1) == 0)
+	    return (void *) hs;
+
+	  /* Adjust filter offset when it doesn't find the mismatch.  */
+	  offset = (offset >= sizeof (long) ? offset : m1) - sizeof (long);
+	}
+
+      /* Skip based on matching the last 2 characters.  */
+      hs += shift1;
+    }
 }
 #endif /* compilation for speed */
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.