mruby-regexp: cache Pike VM state in pattern to avoid per-exec malloc

Pre-allocate visited array and thread lists at compile time and
reuse them across re_exec calls. A cache_in_use flag detects
re-entrancy and falls back to malloc when needed.

Eliminates 3 malloc + 3 free per NFA execution for the common
non-re-entrant case. Combined with the literal fast path, brings
literal match? from 4.7x to 2.5x vs CRuby.

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Yukihiro "Matz" Matsumoto
2026-03-23 11:02:55 +09:00
parent 034a64346c
commit 726d8febf3
3 changed files with 52 additions and 13 deletions
@@ -75,6 +75,11 @@ typedef struct mrb_regexp_pattern {
uint8_t first_bytes[16]; /* bitmap of possible first bytes (128-bit, ASCII) */
mrb_bool has_first_bytes; /* true if first_bytes is usable for skipping */
mrb_bool is_literal; /* true if pattern is pure literal (no metacharacters) */
/* Cached VM state for pike_vm (avoids malloc per re_exec call) */
uint32_t *cached_visited; /* generation-based visited array */
void *cached_threads[2]; /* curr/next thread lists */
int cached_list_capa; /* capacity of cached thread lists */
mrb_bool cache_in_use; /* re-entrancy guard */
} mrb_regexp_pattern;
/* Regexp flags */
@@ -94,6 +99,12 @@ typedef struct mrb_regexp_pattern {
/* Maximum captures */
#define RE_MAX_CAPTURES 32
/* Thread struct for Pike VM (also used for cache sizing) */
typedef struct {
uint32_t pc;
int cap_slot;
} re_thread_cache;
/* Compile a pattern string into bytecode */
mrb_regexp_pattern* re_compile(mrb_state *mrb, const char *pattern, mrb_int len, uint32_t flags);
+13
View File
@@ -902,6 +902,16 @@ re_compile(mrb_state *mrb, const char *pattern, mrb_int len, uint32_t flags)
}
}
/* Pre-allocate VM state cache for pike_vm */
{
int list_capa = (int)pat->code_len * 2 + 16;
pat->cached_visited = (uint32_t*)mrb_calloc(mrb, pat->code_len + 1, sizeof(uint32_t));
pat->cached_threads[0] = mrb_malloc(mrb, sizeof(re_thread_cache) * list_capa);
pat->cached_threads[1] = mrb_malloc(mrb, sizeof(re_thread_cache) * list_capa);
pat->cached_list_capa = list_capa;
pat->cache_in_use = FALSE;
}
if (c.stripped) mrb_free(mrb, c.stripped);
return pat;
}
@@ -914,6 +924,9 @@ re_free(mrb_state *mrb, mrb_regexp_pattern *pat)
mrb_free(mrb, pat->classes);
mrb_free(mrb, pat->named_captures);
mrb_free(mrb, pat->prefix);
mrb_free(mrb, pat->cached_visited);
mrb_free(mrb, pat->cached_threads[0]);
mrb_free(mrb, pat->cached_threads[1]);
mrb_free(mrb, pat);
}
}
+28 -13
View File
@@ -55,10 +55,7 @@ class_match(const re_charclass *cc, uint8_t ch)
* - Threads reference captures by pool index, avoiding 260-byte struct copies
*/
typedef struct {
uint32_t pc;
int cap_slot; /* slot index into capture pool */
} re_thread;
typedef re_thread_cache re_thread;
typedef struct {
re_thread *threads;
@@ -221,6 +218,11 @@ pike_vm(mrb_state *mrb, const mrb_regexp_pattern *pat,
mrb_bool match_only = (captures == NULL || captures_size == 0);
/* Use cached VM state if available (avoids malloc per call) */
mrb_regexp_pattern *mpat = (mrb_regexp_pattern*)pat; /* for cache_in_use flag */
mrb_bool use_cache = !mpat->cache_in_use && mpat->cached_visited != NULL;
if (use_cache) mpat->cache_in_use = TRUE;
pike_state s;
s.mrb = mrb;
s.pat = pat;
@@ -231,7 +233,6 @@ pike_vm(mrb_state *mrb, const mrb_regexp_pattern *pat,
s.match_only = match_only;
s.gen = 1;
if (match_only) {
/* no capture tracking needed; allocate minimal pool (1 dummy slot) */
s.pool_capa = 1;
s.pool_next = 0;
s.cap_pool = (int*)mrb_malloc(mrb, sizeof(int) * ncap);
@@ -244,13 +245,22 @@ pike_vm(mrb_state *mrb, const mrb_regexp_pattern *pat,
s.result_caps = (int*)mrb_malloc(mrb, sizeof(int) * ncap);
memset(s.result_caps, -1, sizeof(int) * ncap);
}
s.visited = (uint32_t*)mrb_calloc(mrb, pat->code_len + 1, sizeof(uint32_t));
re_threadlist curr, next;
curr.threads = (re_thread*)mrb_malloc(mrb, sizeof(re_thread) * list_capa);
curr.count = 0; curr.capa = list_capa;
next.threads = (re_thread*)mrb_malloc(mrb, sizeof(re_thread) * list_capa);
next.count = 0; next.capa = list_capa;
if (use_cache) {
s.visited = mpat->cached_visited;
memset(s.visited, 0, sizeof(uint32_t) * (pat->code_len + 1));
curr.threads = (re_thread*)mpat->cached_threads[0];
next.threads = (re_thread*)mpat->cached_threads[1];
curr.capa = next.capa = mpat->cached_list_capa;
}
else {
s.visited = (uint32_t*)mrb_calloc(mrb, pat->code_len + 1, sizeof(uint32_t));
curr.threads = (re_thread*)mrb_malloc(mrb, sizeof(re_thread) * list_capa);
next.threads = (re_thread*)mrb_malloc(mrb, sizeof(re_thread) * list_capa);
curr.capa = next.capa = list_capa;
}
curr.count = next.count = 0;
for (; sp <= str_end; sp++) {
if (!s.matched) {
@@ -358,11 +368,16 @@ pike_vm(mrb_state *mrb, const mrb_regexp_pattern *pat,
ret = ncap > 0 ? ncap : 1;
}
mrb_free(mrb, curr.threads);
mrb_free(mrb, next.threads);
if (use_cache) {
mpat->cache_in_use = FALSE;
}
else {
mrb_free(mrb, curr.threads);
mrb_free(mrb, next.threads);
mrb_free(mrb, s.visited);
}
mrb_free(mrb, s.cap_pool);
if (s.result_caps) mrb_free(mrb, s.result_caps);
mrb_free(mrb, s.visited);
return ret;
}