From 726d8febf3b35e9d2e3cd07f1ada74ca6dc8de63 Mon Sep 17 00:00:00 2001 From: "Yukihiro \"Matz\" Matsumoto" Date: Mon, 23 Mar 2026 11:02:55 +0900 Subject: [PATCH] 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 --- mrbgems/mruby-regexp/include/re_internal.h | 11 ++++++ mrbgems/mruby-regexp/src/re_compile.c | 13 +++++++ mrbgems/mruby-regexp/src/re_exec.c | 41 +++++++++++++++------- 3 files changed, 52 insertions(+), 13 deletions(-) diff --git a/mrbgems/mruby-regexp/include/re_internal.h b/mrbgems/mruby-regexp/include/re_internal.h index 9a3f8b585..2c66eda1f 100644 --- a/mrbgems/mruby-regexp/include/re_internal.h +++ b/mrbgems/mruby-regexp/include/re_internal.h @@ -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); diff --git a/mrbgems/mruby-regexp/src/re_compile.c b/mrbgems/mruby-regexp/src/re_compile.c index b3ae2d3b6..b43daf3df 100644 --- a/mrbgems/mruby-regexp/src/re_compile.c +++ b/mrbgems/mruby-regexp/src/re_compile.c @@ -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); } } diff --git a/mrbgems/mruby-regexp/src/re_exec.c b/mrbgems/mruby-regexp/src/re_exec.c index f6448e69f..948f6d080 100644 --- a/mrbgems/mruby-regexp/src/re_exec.c +++ b/mrbgems/mruby-regexp/src/re_exec.c @@ -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; }