From 069cc2d1e0dc0e145fe01a0330853391a673b15f Mon Sep 17 00:00:00 2001
From: Devin Hussey <husseydevin@gmail.com>
Date: Wed, 31 Oct 2018 16:49:16 -0400
Subject: [PATCH] Implement the caching regex.c with nregex_t

 - regex.c caches regexes in a hashmap to avoid regcomp/regfree
   hell, and allows us to easily reuse identical regexes.
 - By default, there are 64 entries, and an FNV-1a hash is used.
   Duplicates are handled by putting nregex_t in a two-way linked
   list.
 - It automatically replaces regexec with strstr if it detects
   a string with no special characters.
 - It implements semi-automatic garbage collection to remove
   unreferenced regexes.
   - All regex usage is refcounted, being incremented in nregcomp
     and decremented on nregfree. nregfree only marks it for GC,
     the real one is _nregfree. Probably not the best name, I know.
   - This cleans up a lot of the extra regex from unused syntaxes
     and stuff.
   - Manual GC is called when opening or closing buffers.
   - Otherwise, it will check every 512 times either nregexec,
     nregcomp, or nregfree is called and there are enough unused
     regexes to warrant it, but only when the edit window is not
     NULL, to reduce startup lag.
 - It improves the situation with, but does not fix, #54928.
 - It greatly improves startup performance, especially with a ton
   of rc files:

   macOS 10.13.6
   Gnulib regex engine
   Apple LLVM version 9.1.0 (clang-902.0.39.2) - Clang ~5.0
   MacBook (13-inch, Mid 2009) - MacBook5,2
   2.13 GHz Intel Core 2 Duo
   4 GB RAM

   ~/.nanorc:
        set tabsize 4
        include "~/nano2/syntax/*.nanorc"
        include "~/nano2/syntax/*.nanorc"
        include "~/nano2/syntax/*.nanorc"
        include "~/nano2/syntax/*.nanorc"
        include "~/nano2/syntax/*.nanorc"
        include "~/nano2/syntax/*.nanorc"
        include "~/nano2/syntax/*.nanorc"
        include "~/nano2/syntax/*.nanorc"
        include "~/nano2/syntax/*.nanorc"
        include "~/nano2/syntax/*.nanorc"
        include "~/nano2/syntax/*.nanorc"
        include "~/nano2/syntax/*.nanorc"
        include "~/nano2/syntax/*.nanorc"
        include "~/nano2/syntax/*.nanorc"
        include "~/nano2/syntax/*.nanorc"
        include "~/nano2/syntax/*.nanorc"
        include "~/nano2/syntax/*.nanorc"
        include "~/nano2/syntax/*.nanorc"
        include "~/nano2/syntax/*.nanorc"
        include "~/nano2/syntax/*.nanorc"
        include "~/nano2/syntax/*.nanorc"
        include "~/nano2/syntax/*.nanorc"
        include "~/nano2/syntax/*.nanorc"
        include "~/nano2/syntax/*.nanorc"
        set tabstospaces
        set nowrap
        set mouse
        bind M-Q tabstospaces all
        bind M-S flipreplace all
        bind m-s flipreplace all

   Without the patch:
       $ time ./nano winio.c
       ^X buffered
       real    0m1.550s
       user    0m1.329s
       sys     0m0.060s
       (no notable benefits from a hot cache)
   With the patch:
       (Cold)
       $ time ./nano winio.c
       ^X buffered
       real    0m0.441s
       user    0m0.125s
       sys     0m0.061s
       (Hot)
       $ time ./nano winio.c
       ^X buffered
       real    0m0.227s
       user    0m0.123s
       sys     0m0.062s

Signed-off-by: Devin Hussey <husseydevin@gmail.com>
---
 src/Makefile.am |   1 +
 src/color.c     |  35 +++--
 src/files.c     |   5 +-
 src/global.c    |   4 +-
 src/nano.c      |  11 +-
 src/nano.h      |  30 ++++-
 src/proto.h     |  12 +-
 src/rcfile.c    |  43 ++-----
 src/regex.c     | 333 ++++++++++++++++++++++++++++++++++++++++++++++++
 src/search.c    |  19 +--
 src/text.c      |   2 +-
 src/utils.c     |   8 +-
 src/winio.c     |  18 +--
 13 files changed, 431 insertions(+), 90 deletions(-)
 create mode 100644 src/regex.c

diff --git a/src/Makefile.am b/src/Makefile.am
index 625434c2..706abaeb 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -35,6 +35,7 @@ nano_SOURCES =	browser.c \
 		prompt.c \
 		proto.h \
 		rcfile.c \
+		regex.c \
 		search.c \
 		text.c \
 		utils.c \
diff --git a/src/color.c b/src/color.c
index 6605a713..913bfc04 100644
--- a/src/color.c
+++ b/src/color.c
@@ -141,17 +141,16 @@ void color_init(void)
 bool found_in_list(regexlisttype *head, const char *shibboleth)
 {
 	regexlisttype *item;
-	regex_t rgx;
+	nregex_t *rgx;
 
 	for (item = head; item != NULL; item = item->next) {
-		regcomp(&rgx, fixbounds(item->full_regex), NANO_REG_EXTENDED);
+		rgx = nregcomp(item->full_regex, FALSE);
 
-		if (regexec(&rgx, shibboleth, 0, NULL, 0) == 0) {
-			regfree(&rgx);
+		if (rgx && nregexec(rgx, shibboleth, 0, NULL, 0) == 0) {
+			nregfree(rgx);
 			return TRUE;
 		}
-
-		regfree(&rgx);
+		nregfree(rgx);
 	}
 
 	return FALSE;
@@ -165,13 +164,13 @@ void color_update(void)
 
 	/* If the rcfiles were not read, or contained no syntaxes, get out. */
 	if (syntaxes == NULL)
-		return;
+		goto end;
 
 	/* If we specified a syntax-override string, use it. */
 	if (syntaxstr != NULL) {
 		/* An override of "none" is like having no syntax at all. */
 		if (strcmp(syntaxstr, "none") == 0)
-			return;
+			goto end;
 
 		for (sint = syntaxes; sint != NULL; sint = sint->next) {
 			if (strcmp(sint->name, syntaxstr) == 0)
@@ -195,7 +194,7 @@ void color_update(void)
 		else {
 			/* Concatenate the current working directory with the
 			 * specified filename, and canonicalize the result. */
-			sprintf(joinednames, "%s/%s", currentdir, openfile->filename);
+			snprintf(joinednames, PATH_MAX, "%s/%s", currentdir, openfile->filename);
 			fullname = get_full_path(joinednames);
 			free(currentdir);
 		}
@@ -272,15 +271,15 @@ void color_update(void)
 	 * already been checked for validity when they were read in). */
 	for (ink = openfile->colorstrings; ink != NULL; ink = ink->next) {
 		if (ink->start == NULL) {
-			ink->start = (regex_t *)nmalloc(sizeof(regex_t));
-			regcomp(ink->start, fixbounds(ink->start_regex), ink->rex_flags);
+			ink->start = nregcomp(fixbounds(ink->start_regex), !(ink->rex_flags & REG_ICASE));
 		}
 
 		if (ink->end_regex != NULL && ink->end == NULL) {
-			ink->end = (regex_t *)nmalloc(sizeof(regex_t));
-			regcomp(ink->end, fixbounds(ink->end_regex), ink->rex_flags);
+			ink->end = nregcomp(fixbounds(ink->end_regex), !(ink->rex_flags & REG_ICASE));
 		}
 	}
+end:
+	nregex_gc();
 }
 
 /* Determine whether the matches of multiline regexes are still the same,
@@ -303,9 +302,9 @@ void check_the_multis(filestruct *line)
 
 		alloc_multidata_if_needed(line);
 
-		astart = (regexec(ink->start, line->data, 1, &startmatch, 0) == 0);
+		astart = (nregexec(ink->start, line->data, 1, &startmatch, 0) == 0);
 		afterstart = line->data + (astart ? startmatch.rm_eo : 0);
-		anend = (regexec(ink->end, afterstart, 1, &endmatch, 0) == 0);
+		anend = (nregexec(ink->end, afterstart, 1, &endmatch, 0) == 0);
 
 		/* Check whether the multidata still matches the current situation. */
 		if (line->multidata[ink->id] == CNONE ||
@@ -377,14 +376,14 @@ void precalc_multicolorinfo(void)
 
 			/* When the line contains a start match, look for an end, and if
 			 * found, mark all the lines that are affected. */
-			while (regexec(ink->start, line->data + index, 1,
+			while (nregexec(ink->start, line->data + index, 1,
 						&startmatch, (index == 0) ? 0 : REG_NOTBOL) == 0) {
 				/* Begin looking for an end match after the start match. */
 				index += startmatch.rm_eo;
 
 				/* If there is an end match on this line, mark the line, but
 				 * continue looking for other starts after it. */
-				if (regexec(ink->end, line->data + index, 1,
+				if (nregexec(ink->end, line->data + index, 1,
 						&endmatch, (index == 0) ? 0 : REG_NOTBOL) == 0) {
 					line->multidata[ink->id] = CSTARTENDHERE;
 					index += endmatch.rm_eo;
@@ -403,7 +402,7 @@ void precalc_multicolorinfo(void)
 				tailline = line->next;
 
 				while (tailline != NULL) {
-					if (regexec(ink->end, tailline->data, 1, &endmatch, 0) == 0)
+					if (nregexec(ink->end, tailline->data, 1, &endmatch, 0) == 0)
 						break;
 					tailline = tailline->next;
 				}
diff --git a/src/files.c b/src/files.c
index 5878ff00..4169f98b 100644
--- a/src/files.c
+++ b/src/files.c
@@ -683,8 +683,10 @@ void switch_to_next_buffer(void)
 bool close_buffer(void)
 {
 	/* If only one file buffer is open, get out. */
-	if (openfile == openfile->next)
+	if (openfile == openfile->next) {
+		nregex_gc();
 		return FALSE;
+	}
 
 #ifdef ENABLE_HISTORIES
 	if (ISSET(POSITIONLOG))
@@ -704,6 +706,7 @@ bool close_buffer(void)
 	if (openfile == openfile->next)
 		exitfunc->desc = exit_tag;
 
+	nregex_gc();
 	return TRUE;
 }
 #endif /* ENABLE_MULTIBUFFER */
diff --git a/src/global.c b/src/global.c
index e1d7930f..1bce6fd6 100644
--- a/src/global.c
+++ b/src/global.c
@@ -140,7 +140,7 @@ char *brackets = NULL;
 		 * can end sentences. */
 char *quotestr = NULL;
 		/* The quoting string.  The default value is set in main(). */
-regex_t quotereg;
+nregex_t *quotereg = NULL;
 		/* The compiled regular expression from the quoting string. */
 int quoterc;
 		/* Whether it was compiled successfully. */
@@ -224,7 +224,7 @@ poshiststruct *position_history = NULL;
 		/* The list of filenames with their last cursor positions. */
 #endif
 
-regex_t search_regexp;
+nregex_t *search_regexp = NULL;
 		/* The compiled regular expression to use in searches. */
 regmatch_t regmatches[10];
 		/* The match positions for parenthetical subexpressions, 10
diff --git a/src/nano.c b/src/nano.c
index 8c42eaa9..89aa066b 100644
--- a/src/nano.c
+++ b/src/nano.c
@@ -589,6 +589,8 @@ void finish(void)
 	}
 #endif
 
+	nregfree_all();
+
 	/* Get out. */
 	exit(0);
 }
@@ -2470,15 +2472,10 @@ int main(int argc, char **argv)
 
 	/* Compile the quoting regex, and free it when it's good; otherwise,
 	 * retrieve and store the error message, to be shown when justifying. */
-	quoterc = regcomp(&quotereg, quotestr, NANO_REG_EXTENDED);
-	if (quoterc == 0) {
+	quotereg = nregcomp(quotestr, FALSE);
+	if (quotereg != NULL) {
 		free(quotestr);
 		quotestr = NULL;
-	} else {
-		size_t size = regerror(quoterc, &quotereg, NULL, 0);
-
-		quoteerr = charalloc(size);
-		regerror(quoterc, &quotereg, quoteerr, size);
 	}
 #endif /* ENABLE_JUSTIFY */
 
diff --git a/src/nano.h b/src/nano.h
index 3007881d..5e171817 100644
--- a/src/nano.h
+++ b/src/nano.h
@@ -142,10 +142,8 @@
 #define REPLACING 1
 #define INREGION  2
 
-#ifdef ENABLE_COLOR
 #define USE_THE_DEFAULT -1
 #define BAD_COLOR -2
-#endif
 
 /* Enumeration types. */
 typedef enum {
@@ -182,6 +180,29 @@ typedef enum {
 } undo_type;
 
 /* Structure types. */
+
+/* A regex_t wrapper. We store nregex_t in a hash map to avoid endless regex
+ * recompilations. */
+typedef struct nregex_type {
+	regex_t *regex;
+		/* The compiled regex itself. If trivial is true, this will be NULL. */
+	char *regex_str;
+		/* The regex string. This is strcmp'd to check for collisions. */
+	struct nregex_type *next;
+		/* Next in the list, in case we have collisions. */
+	struct nregex_type *prev;
+		/* Last in the list, mainly for GC */
+	uint32_t hash;
+		/* The actual hash, to avoid either rehashing regex_str or strcmp. */
+	uint16_t refcount;
+		/* How many references we have. */
+	bool case_sensitive;
+		/* Whether this regex is case sensitive. */
+	bool trivial;
+		/* Whether this is a trivial string and we don't have to waste our
+		 * time with actual regex. */
+} nregex_t;
+
 #ifdef ENABLE_COLOR
 typedef struct colortype {
 	short fg;
@@ -197,11 +218,11 @@ typedef struct colortype {
 		/* The regex compilation flags (with or without REG_ICASE). */
 	char *start_regex;
 		/* The start (or all) of the regex string. */
-	regex_t *start;
+	nregex_t *start;
 		/* The compiled start (or all) of the regex string. */
 	char *end_regex;
 		/* The end (if any) of the regex string. */
-	regex_t *end;
+	nregex_t *end;
 		/* The compiled end (if any) of the regex string. */
 	struct colortype *next;
 		/* Next set of colors. */
@@ -212,6 +233,7 @@ typedef struct colortype {
 typedef struct regexlisttype {
 	char *full_regex;
 		/* A regex string to match things that imply a certain syntax. */
+	nregex_t *regex;
 	struct regexlisttype *next;
 		/* The next regex. */
 } regexlisttype;
diff --git a/src/proto.h b/src/proto.h
index a157ece0..be6d341c 100644
--- a/src/proto.h
+++ b/src/proto.h
@@ -108,7 +108,7 @@ extern const char *unjust_tag;
 extern char *punct;
 extern char *brackets;
 extern char *quotestr;
-extern regex_t quotereg;
+extern nregex_t *quotereg;
 extern int quoterc;
 extern char *quoteerr;
 #endif
@@ -159,7 +159,7 @@ extern filestruct *executebot;
 extern poshiststruct *position_history;
 #endif
 
-extern regex_t search_regexp;
+extern nregex_t *search_regexp;
 extern regmatch_t regmatches[10];
 
 extern int hilite_attribute;
@@ -469,6 +469,14 @@ void parse_rcfile(FILE *rcstream, bool syntax_only);
 void do_rcfiles(void);
 #endif /* ENABLE_NANORC */
 
+/* Most functions in regex.c */
+nregex_t *nregcomp(const char *restrict regex_str_raw, bool case_sensitive);
+int nregexec(const nregex_t *restrict nregex, const char *restrict str,
+             size_t nmatch, regmatch_t pmatch[], int eflags);
+void nregfree_all(void);
+void nregfree(nregex_t *restrict nregex);
+void nregex_gc(void);
+
 /* Most functions in search.c. */
 void tidy_up_after_search(void);
 int findnextstr(const char *needle, bool whole_word_only, int modus,
diff --git a/src/rcfile.c b/src/rcfile.c
index d8a2d1fe..5ff0a683 100644
--- a/src/rcfile.c
+++ b/src/rcfile.c
@@ -237,27 +237,6 @@ char *parse_next_regex(char *ptr)
 	return ptr;
 }
 
-/* Compile the regular expression regex to see if it's valid.  Return
- * TRUE if it is, and FALSE otherwise. */
-bool nregcomp(const char *regex, int compile_flags)
-{
-	regex_t preg;
-	const char *r = fixbounds(regex);
-	int rc = regcomp(&preg, r, compile_flags);
-
-	if (rc != 0) {
-		size_t len = regerror(rc, &preg, NULL, 0);
-		char *str = charalloc(len);
-
-		regerror(rc, &preg, str, len);
-		rcfile_error(N_("Bad regex \"%s\": %s"), r, str);
-		free(str);
-	}
-
-	regfree(&preg);
-	return (rc == 0);
-}
-
 /* Parse the next syntax name and its possible extension regexes from the
  * line at ptr, and add it to the global linked list of color syntaxes. */
 void parse_syntax(char *ptr)
@@ -671,10 +650,9 @@ void parse_colors(char *ptr, int rex_flags)
 	while (ptr != NULL && *ptr != '\0') {
 		colortype *newcolor = NULL;
 			/* The container for a color plus its regexes. */
-		bool goodstart;
-			/* Whether the start expression was valid. */
 		bool expectend = FALSE;
 			/* Whether to expect an end= line. */
+		nregex_t *nregex;
 
 		if (strncasecmp(ptr, "start=", 6) == 0) {
 			ptr += 6;
@@ -694,13 +672,13 @@ void parse_colors(char *ptr, int rex_flags)
 
 		if (*item == '\0') {
 			rcfile_error(N_("Empty regex string"));
-			goodstart = FALSE;
+			nregex = NULL;
 		} else
-			goodstart = nregcomp(item, rex_flags);
+			nregex = nregcomp(item, (rex_flags & REG_ICASE) == 0);
 
 		/* If the starting regex is valid, initialize a new color struct,
 		 * and hook it in at the tail of the linked list. */
-		if (goodstart) {
+		if (nregex != NULL) {
 			newcolor = (colortype *)nmalloc(sizeof(colortype));
 
 			newcolor->fg = fg;
@@ -722,6 +700,7 @@ void parse_colors(char *ptr, int rex_flags)
 				lastcolor->next = newcolor;
 
 			lastcolor = newcolor;
+			nregfree(nregex);
 		}
 
 		if (!expectend)
@@ -750,13 +729,14 @@ void parse_colors(char *ptr, int rex_flags)
 
 		/* If the start regex was invalid, skip past the end regex
 		 * to stay in sync. */
-		if (!goodstart)
+		if (nregex == NULL)
 			continue;
 
 		/* If it's valid, save the ending regex string. */
-		if (nregcomp(item, rex_flags))
+		if ((nregex = nregcomp(item, (rex_flags & REG_ICASE) == 0)) != NULL) {
 			newcolor->end_regex = mallocstrcpy(NULL, item);
-
+			nregfree(nregex);
+		}
 		/* Lame way to skip another static counter. */
 		newcolor->id = live_syntax->nmultis;
 		live_syntax->nmultis++;
@@ -812,6 +792,7 @@ void grab_and_store(const char *kind, char *ptr, regexlisttype **storage)
 	while (*ptr != '\0') {
 		const char *regexstring;
 		regexlisttype *newthing;
+		nregex_t *nregex;
 
 		if (*ptr != '"') {
 			rcfile_error(N_("Regex strings must begin and end with a \" character"));
@@ -824,13 +805,15 @@ void grab_and_store(const char *kind, char *ptr, regexlisttype **storage)
 			return;
 
 		/* If the regex string is malformed, skip it. */
-		if (!nregcomp(regexstring, NANO_REG_EXTENDED | REG_NOSUB))
+		if ((nregex = nregcomp(regexstring, TRUE)) == NULL)
 			continue;
 
 		/* Copy the regex into a struct, and hook this in at the end. */
 		newthing = (regexlisttype *)nmalloc(sizeof(regexlisttype));
 		newthing->full_regex = mallocstrcpy(NULL, regexstring);
 		newthing->next = NULL;
+		newthing->regex = NULL;
+		nregfree(nregex);
 
 		if (lastthing == NULL)
 			*storage = newthing;
diff --git a/src/regex.c b/src/regex.c
new file mode 100644
index 00000000..bed7851b
--- /dev/null
+++ b/src/regex.c
@@ -0,0 +1,333 @@
+/**************************************************************************
+ *   regex.c  --  This file is part of GNU nano.                          *
+ *                                                                        *
+ *   Copyright (C) 2018 Free Software Foundation, Inc.                    *
+ *   Copyright (C) 2018 Benno Schulenberg                                 *
+ *                                                                        *
+ *   GNU nano is free software: you can redistribute it and/or modify     *
+ *   it under the terms of the GNU General Public License as published    *
+ *   by the Free Software Foundation, either version 3 of the License,    *
+ *   or (at your option) any later version.                               *
+ *                                                                        *
+ *   GNU nano is distributed in the hope that it will be useful,          *
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty          *
+ *   of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.              *
+ *   See the GNU General Public License for more details.                 *
+ *                                                                        *
+ *   You should have received a copy of the GNU General Public License    *
+ *   along with this program.  If not, see http://www.gnu.org/licenses/.  *
+ *                                                                        *
+ **************************************************************************/
+
+/* Manages a hashmap of cached regexes. */
+#include "proto.h"
+#include <string.h>
+#include <limits.h>
+
+/* Must be a power of 2 for performance! */
+#define REGEX_HASHMAP_SIZE 64u
+
+static nregex_t *regex_hashmap[REGEX_HASHMAP_SIZE] = {0};
+static size_t regex_hashmap_count = 0;
+static size_t regex_total_referenced = 0;
+
+/* The interval for automatic garbage collection. */
+#define REGEX_GC_INTERVAL 512
+static size_t regex_gc_timer = REGEX_GC_INTERVAL;
+
+#define NUM_GC_ITEMS ((REGEX_HASHMAP_SIZE / (8 * sizeof(unsigned))) ?: 1)
+static unsigned marked_gc[NUM_GC_ITEMS] = {0};
+#define ELEMENT_SIZE (sizeof(unsigned) * 8)
+
+/* Yes, it is the same as the FLAGS macros. */
+#define TO_FREE(bucket) marked_gc[((bucket) / ELEMENT_SIZE)]
+#define BUCKETMASK(bucket) ((size_t)1U << ((bucket) % ELEMENT_SIZE))
+#define GC_MARK(bucket) TO_FREE(bucket) |= BUCKETMASK(bucket)
+#define GC_UNMARK(bucket) TO_FREE(bucket) &= ~BUCKETMASK(bucket)
+#define GC_MARKED(bucket) ((TO_FREE(bucket) & BUCKETMASK(bucket)) != 0)
+
+/* Actually frees an individual regex struct. See nregfree which marks it for
+ * deletion. */
+static void _nregfree(nregex_t *nregex)
+{
+	if (nregex == NULL)
+		return;
+
+	if (!nregex->trivial && nregex->regex != NULL) {
+		regfree(nregex->regex);
+		free(nregex->regex);
+	}
+	/* We have a parent */
+	if (nregex->prev != NULL) {
+		nregex->prev->next = nregex->next;
+		if (nregex->next != NULL) {
+			nregex->next->prev = nregex->prev;
+		}
+	} else {
+		regex_hashmap[nregex->hash % REGEX_HASHMAP_SIZE] = nregex->next;
+		if (nregex->next != NULL) {
+			nregex->next->prev = NULL;
+		}
+	}
+	free(nregex->regex_str);
+	free(nregex);
+}
+
+/* Recursively frees the regex structs in the index i.
+ * If force is TRUE, the regexes will be freed unconditionally, but otherwise,
+ * only the ones without any references are freed. */
+void nregfree_recursive(unsigned i, bool force)
+{
+	nregex_t *nregex = regex_hashmap[i];
+
+	while (nregex != NULL) {
+		nregex_t *tmp = nregex;
+		nregex = nregex->next;
+		if (force) {
+			_nregfree(tmp);
+			--regex_hashmap_count;
+		} else {
+			if (tmp->refcount == 0) {
+				_nregfree(tmp);
+				--regex_hashmap_count;
+			}
+		}
+	}
+}
+
+/* Checks for any unreferenced regexes and frees them. */
+void nregex_gc(void)
+{
+	/* Forcibly reset the timer in case we manually called nregex_gc */
+	regex_gc_timer = REGEX_GC_INTERVAL;
+
+	/* Don't do anything if we are full */
+	if (regex_hashmap_count == regex_total_referenced)
+		return;
+
+	for (unsigned i = 0; i < sizeof(marked_gc) / sizeof(marked_gc[0]); i++) {
+		/* First check the array item itself before comparing bits. */
+		if (marked_gc[i] != 0) {
+			/* Check the bits, only freeing if they are marked */
+			for (unsigned j = 0; j < ELEMENT_SIZE; j++) {
+				if (GC_MARKED((i * ELEMENT_SIZE) + j)) {
+					nregfree_recursive((i * ELEMENT_SIZE) + j, FALSE);
+				}
+			}
+			/* Unmark them all at once. */
+			marked_gc[i] = 0;
+		}
+	}
+
+}
+
+static inline void nregex_gc_check(void)
+{
+	/* Run garbage collection if the following conditions are met:
+	 *
+	 * - The regex timer is zero
+	 * - There are things that can actually be freed
+	 * - The editor window is not NULL, as we don't want to lag startup
+	 * - Either...
+	 *   - The hashmap has half as many entries as buckets and less than 1/16
+	 *     on them are referenced
+	 *   - The hashmap is 4 times as many entries as buckets and less than
+	 *     half of them are referenced
+	 *   - The hashmap is 16 times as many entries as buckets and there are
+	 *     any referenced at all.
+	 */
+	if (--regex_gc_timer == 0
+	    && regex_total_referenced < regex_hashmap_count
+	    && edit != NULL
+	    && (
+	        (regex_hashmap_count >= REGEX_HASHMAP_SIZE / 2
+	            && regex_total_referenced < regex_hashmap_count / 16)
+	        || (regex_hashmap_count >= REGEX_HASHMAP_SIZE * 4
+	            && regex_total_referenced < regex_hashmap_count / 2)
+	        || (regex_hashmap_count >= REGEX_HASHMAP_SIZE * 16)
+	)) {
+		nregex_gc();
+	}
+}
+
+/* Marks a regex for freeing without actually freeing it. */
+void nregfree(nregex_t *nregex)
+{
+	if (nregex != NULL) {
+		if (nregex->refcount > 1) {
+			nregex->refcount--;
+		} else {
+			regex_total_referenced--;
+			nregex->refcount = 0;
+			/* Mark the GC table that we have a match. */
+			GC_MARK(nregex->hash % REGEX_HASHMAP_SIZE);
+		}
+	}
+	nregex_gc_check();
+}
+
+/* Free every entry in the hash table. */
+void nregfree_all(void)
+{
+	for (size_t i = 0; i < REGEX_HASHMAP_SIZE; i++) {
+		if (regex_hashmap[i] != NULL) {
+			nregfree_recursive(i, TRUE);
+			regex_hashmap[i] = NULL;
+		}
+	}
+}
+
+/* Same as regexec, but takes nregex and uses strstr if the regex is trivial. */
+int nregexec(const nregex_t *restrict nregex, const char *restrict str,
+             size_t nmatch, regmatch_t pmatch[], int eflags)
+{
+	if (nregex == NULL)
+		return REG_NOMATCH;
+
+	nregex_gc_check();
+	/* If we have a simple regex string, we can just use strstr or
+	 * strcasestr. */
+	if (nregex->trivial || nregex->regex == NULL) {
+		const char *match = (nregex->case_sensitive)
+			? strstr(str, nregex->regex_str)
+			: strcasestr(str, nregex->regex_str);
+
+		if (pmatch != NULL && nmatch > 0) {
+			if (match == NULL) {
+				pmatch[0].rm_so = -1;
+				pmatch[1].rm_eo = -1;
+			} else {
+				pmatch[0].rm_so = match - str;
+				pmatch[0].rm_eo = pmatch[0].rm_so + strlen(nregex->regex_str);
+			}
+			/* We can only have one match, so mark any others as not found. */
+			for (int i = 1; i < nmatch; i++) {
+				pmatch[i].rm_so = -1;
+				pmatch[i].rm_eo = -1;
+			}
+		}
+
+		return (match == NULL) ? REG_NOMATCH : 0;
+	} else {
+		if (nregex->regex == NULL) {
+			return REG_NOMATCH;
+		}
+		return regexec(nregex->regex, str, nmatch, pmatch, eflags);
+	}
+}
+
+/* Finds an nregex_t from the hash map and returns it.
+ * If there is no match, either a new nregex_t is created (create == TRUE),
+ * or a null pointer is returned. */
+static nregex_t *find_regex(const char *restrict regex_str,
+                            bool case_sensitive, bool create)
+{
+	nregex_t *prev = NULL;
+	nregex_t *nregex = NULL;
+
+	/* FNV-1a hashing algorithm. */
+    #define FNV_PRIME  16777619UL
+    #define FNV_OFFSET 2166136261UL
+	uint32_t hash = FNV_OFFSET;
+	{
+		const uint8_t *restrict data = (const uint8_t *)regex_str;
+
+		while (*data)
+			hash = (hash ^ *data++) * FNV_PRIME;
+	}
+
+	/* Go through the matches. We have to check because FNV-1a is not perfect,
+	 * and we also only have so many unique keys. */
+	nregex = regex_hashmap[hash % REGEX_HASHMAP_SIZE];
+
+	for (; nregex != NULL; nregex = nregex->next) {
+		/* Check if we already have an identical regex, and return it if so.
+		 * If we have more refcounts than we can handle, we have to create a
+		 * new one to be safe. */
+		if (nregex->regex_str != NULL
+		    && nregex->hash == hash
+		    && nregex->case_sensitive == case_sensitive
+		    && nregex->refcount < UINT16_MAX
+		    && strcmp(nregex->regex_str, regex_str) == 0) {
+			if (nregex->refcount++ == 0)
+				regex_total_referenced ++;
+			return nregex;
+		}
+		/* Collision or same regex with different flags. Store nregex as the
+		 * last one so we can potentially link it in to the hash list. */
+		prev = nregex;
+	}
+
+	/* Create a new nregex_t if it doesn't exist. */
+	if (create) {
+		nregex = (nregex_t *)nmalloc(sizeof(nregex_t));
+		nregex->regex_str = mallocstrcpy(NULL, regex_str);
+		nregex->hash = hash;
+		nregex->next = NULL;
+		nregex->case_sensitive = case_sensitive;
+
+		/* Check for regex metacharacters. We don't bother checking for escapes,
+		 * as checking for proper regex...needs regex, and we also have to
+		 * modify the string in order to remove double backslashes. */
+		nregex->trivial = (strpbrk(regex_str, "[](){}.*+?^\\|") == NULL);
+
+		/* Only compile the regex if the string is not trivial and it hasn't
+		 * already been compiled. */
+		if (nregex->trivial) {
+			nregex->regex = NULL;
+		} else {
+			/* Allocate the regex */
+			nregex->regex = (regex_t *)nmalloc(sizeof(regex_t));
+			int rc = regcomp(nregex->regex, nregex->regex_str,
+			                 NANO_REG_EXTENDED|(case_sensitive ? 0 : REG_ICASE));
+
+			/* Error */
+			if (rc != 0) {
+				/* 128 bytes should be more than enough. */
+				char buf[128];
+				regerror(rc, nregex->regex, buf, 128);
+
+				statusline(ALERT, N_("Bad regex \"%s\": %s"),
+				           nregex->regex_str, buf);
+
+				/* Clean up and return NULL. */
+				regfree(nregex->regex);
+				free(nregex->regex);
+				free(nregex->regex_str);
+				free(nregex);
+
+				return NULL;
+			}
+		}
+		if (prev != NULL) {
+			/* Link it with the previous */
+			prev->next = nregex;
+			nregex->prev = prev;
+		} else {
+			/* Put it at the top of the list */
+			nregex->prev = NULL;
+			regex_hashmap[hash % REGEX_HASHMAP_SIZE] = nregex;
+		}
+		++regex_hashmap_count;
+
+		nregex->refcount = 1;
+		++regex_total_referenced;
+		return nregex;
+	}
+	return NULL;
+}
+
+/* Returns a nregex_t generated from the arguments. Either this will return
+ * a previously generated nregex_t, or it will create a new one.
+ * On an error, the error message is statuslined, and this will return NULL. */
+nregex_t *nregcomp(const char *restrict regex_str_raw, bool case_sensitive)
+{
+	/* Fix word boundaries first */
+	const char *regex_str = fixbounds(regex_str_raw);
+	/* Find the regex we need */
+	nregex_t *restrict nregex = find_regex(regex_str, case_sensitive, TRUE);
+
+	/* Note: the one we just found is guaranteed not to be freed. */
+	nregex_gc_check();
+	return nregex;
+}
diff --git a/src/search.c b/src/search.c
index a8b14291..2035cf89 100644
--- a/src/search.c
+++ b/src/search.c
@@ -35,18 +35,13 @@ static bool have_compiled_regexp = FALSE;
  * Return TRUE if the expression is valid, and FALSE otherwise. */
 bool regexp_init(const char *regexp)
 {
-	int value = regcomp(&search_regexp, fixbounds(regexp),
-				NANO_REG_EXTENDED | (ISSET(CASE_SENSITIVE) ? 0 : REG_ICASE));
+	if (search_regexp != NULL) {
+		nregfree(search_regexp);
+	}
+	search_regexp = nregcomp(fixbounds(regexp), ISSET(CASE_SENSITIVE));
 
 	/* If regex compilation failed, show the error message. */
-	if (value != 0) {
-		size_t len = regerror(value, &search_regexp, NULL, 0);
-		char *str = charalloc(len);
-
-		regerror(value, &search_regexp, str, len);
-		statusline(ALERT, _("Bad regex \"%s\": %s"), regexp, str);
-		free(str);
-
+	if (search_regexp == NULL) {
 		return FALSE;
 	}
 
@@ -60,7 +55,7 @@ bool regexp_init(const char *regexp)
 void tidy_up_after_search(void)
 {
 	if (have_compiled_regexp) {
-		regfree(&search_regexp);
+		nregfree(search_regexp);
 		have_compiled_regexp = FALSE;
 	}
 #ifndef NANO_TINY
@@ -439,7 +434,7 @@ int replace_regexp(char *string, bool create)
 	while (*c != '\0') {
 		int num = (*(c + 1) - '0');
 
-		if (*c != '\\' || num < 1 || num > 9 || num > search_regexp.re_nsub) {
+		if (*c != '\\' || num < 1 || num > 9 || (search_regexp->regex && num > search_regexp->regex->re_nsub)) {
 			if (create)
 				*string++ = *c;
 			c++;
diff --git a/src/text.c b/src/text.c
index 8302c7dd..f2ba7ed5 100644
--- a/src/text.c
+++ b/src/text.c
@@ -1974,7 +1974,7 @@ void justify_format(filestruct *paragraph, size_t skip)
 size_t quote_length(const char *line)
 {
 	regmatch_t matches;
-	int rc = regexec(&quotereg, line, 1, &matches, 0);
+	int rc = nregexec(quotereg, line, 1, &matches, 0);
 
 	if (rc == REG_NOMATCH || matches.rm_so == (regoff_t)-1)
 		return 0;
diff --git a/src/utils.c b/src/utils.c
index fdbe73a3..1d13b815 100644
--- a/src/utils.c
+++ b/src/utils.c
@@ -279,7 +279,7 @@ const char *strstrwrapper(const char *haystack, const char *needle,
 			size_t floor = 0, next_rung = 0;
 				/* The start of the search range, and the next start. */
 
-			if (regexec(&search_regexp, haystack, 1, regmatches, 0) != 0)
+			if (nregexec(search_regexp, haystack, 1, regmatches, 0) != 0)
 				return NULL;
 
 			far_end = strlen(haystack);
@@ -301,7 +301,7 @@ const char *strstrwrapper(const char *haystack, const char *needle,
 				next_rung = move_mbright(haystack, last_find);
 				regmatches[0].rm_so = next_rung;
 				regmatches[0].rm_eo = far_end;
-				if (regexec(&search_regexp, haystack, 1, regmatches,
+				if (nregexec(search_regexp, haystack, 1, regmatches,
 										REG_STARTEND) != 0)
 					break;
 			}
@@ -309,7 +309,7 @@ const char *strstrwrapper(const char *haystack, const char *needle,
 			/* Find the last match again, to get possible submatches. */
 			regmatches[0].rm_so = floor;
 			regmatches[0].rm_eo = far_end;
-			if (regexec(&search_regexp, haystack, 10, regmatches,
+			if (nregexec(search_regexp, haystack, 10, regmatches,
 										REG_STARTEND) != 0)
 				return NULL;
 
@@ -319,7 +319,7 @@ const char *strstrwrapper(const char *haystack, const char *needle,
 		/* Do a forward regex search from the starting point. */
 		regmatches[0].rm_so = start - haystack;
 		regmatches[0].rm_eo = strlen(haystack);
-		if (regexec(&search_regexp, haystack, 10, regmatches,
+		if (nregexec(search_regexp, haystack, 10, regmatches,
 										REG_STARTEND) != 0)
 			return NULL;
 		else
diff --git a/src/winio.c b/src/winio.c
index 5a98456d..aad2d547 100644
--- a/src/winio.c
+++ b/src/winio.c
@@ -2462,7 +2462,7 @@ void edit_draw(filestruct *fileptr, const char *converted,
 					 * unless index is zero.  If regexec() returns
 					 * REG_NOMATCH, there are no more matches in the
 					 * line. */
-					if (regexec(varnish->start, &fileptr->data[index], 1,
+					if (nregexec(varnish->start, &fileptr->data[index], 1,
 								&match, (index == 0) ? 0 : REG_NOTBOL) != 0)
 						break;
 
@@ -2520,11 +2520,11 @@ void edit_draw(filestruct *fileptr, const char *converted,
 
 			/* First step: see if there is a line before current that
 			 * matches 'start' and is not complemented by an 'end'. */
-			while (start_line != NULL && regexec(varnish->start,
+			while (start_line != NULL && nregexec(varnish->start,
 					start_line->data, 1, &startmatch, 0) == REG_NOMATCH) {
 				/* There is no start on this line; but if there is an end,
 				 * there is no need to look for starts on earlier lines. */
-				if (regexec(varnish->end, start_line->data, 0, NULL, 0) == 0)
+				if (nregexec(varnish->end, start_line->data, 0, NULL, 0) == 0)
 					goto step_two;
 				start_line = start_line->prev;
 			}
@@ -2545,7 +2545,7 @@ void edit_draw(filestruct *fileptr, const char *converted,
 				/* Begin searching for an end after the start match. */
 				index += startmatch.rm_eo;
 				/* If there is no end after this last start, good. */
-				if (regexec(varnish->end, start_line->data + index, 1, &endmatch,
+				if (nregexec(varnish->end, start_line->data + index, 1, &endmatch,
 								(index == 0) ? 0 : REG_NOTBOL) == REG_NOMATCH)
 					break;
 				/* Begin searching for a new start after the end match. */
@@ -2558,7 +2558,7 @@ void edit_draw(filestruct *fileptr, const char *converted,
 					index = move_mbright(start_line->data, index);
 				}
 				/* If there is no later start on this line, next step. */
-				if (regexec(varnish->start, start_line->data + index,
+				if (nregexec(varnish->start, start_line->data + index,
 								1, &startmatch, REG_NOTBOL) == REG_NOMATCH)
 					goto step_two;
 			}
@@ -2568,7 +2568,7 @@ void edit_draw(filestruct *fileptr, const char *converted,
 			/* We've already checked that there is no end between the start
 			 * and the current line.  But is there an end after the start
 			 * at all?  We don't paint unterminated starts. */
-			while (end_line != NULL && regexec(varnish->end, end_line->data,
+			while (end_line != NULL && nregexec(varnish->end, end_line->data,
 								1, &endmatch, 0) == REG_NOMATCH)
 				end_line = end_line->next;
 
@@ -2598,7 +2598,7 @@ void edit_draw(filestruct *fileptr, const char *converted,
 			 * looking only after an end match, if there is one. */
 			index = (paintlen == 0) ? 0 : endmatch.rm_eo;
 
-			while (regexec(varnish->start, fileptr->data + index,
+			while (nregexec(varnish->start, fileptr->data + index,
 								1, &startmatch, (index == 0) ?
 								0 : REG_NOTBOL) == 0) {
 				/* Translate the match to be relative to the
@@ -2612,7 +2612,7 @@ void edit_draw(filestruct *fileptr, const char *converted,
 
 				thetext = converted + actual_x(converted, start_col);
 
-				if (regexec(varnish->end, fileptr->data + startmatch.rm_eo,
+				if (nregexec(varnish->end, fileptr->data + startmatch.rm_eo,
 								1, &endmatch, (startmatch.rm_eo == 0) ?
 								0 : REG_NOTBOL) == 0) {
 					/* Translate the end match to be relative to
@@ -2645,7 +2645,7 @@ void edit_draw(filestruct *fileptr, const char *converted,
 				/* There is no end on this line.  But maybe on later lines? */
 				end_line = fileptr->next;
 
-				while (end_line != NULL && regexec(varnish->end, end_line->data,
+				while (end_line != NULL && nregexec(varnish->end, end_line->data,
 										0, NULL, 0) == REG_NOMATCH)
 					end_line = end_line->next;
 
-- 
2.19.0

