Skip to content

perf(parse): give each thread its own regex cache pool - #13

Merged
jqnatividad merged 1 commit into
mainfrom
thread-local-regex-pools
Oct 3, 2026
Merged

jqnatividad merged 1 commit into
mainfrom
thread-local-regex-pools

Conversation

@jqnatividad

Copy link
Copy Markdown
Collaborator

Summary

regex! returned one global Regex per pattern. regex-automata gives only the first thread that uses a Regex the lock-free cache fast path; every other thread goes through mutex-protected cache stacks (util/pool.rs, MAX_POOL_STACKS = 8). Under a parallel caller like qsv stats --infer-dates, that contention ate ~10% of busy CPU (pthread_mutex_*, Pool::get_slow, Pool::put_value in a samply profile), and date inference cost +1.41 s CPU on 16 threads vs +0.75 s single-threaded.

regex! now returns a &'static LocalKey<Regex> holding a per-thread clone of the once-compiled regex. Regex::clone shares the compiled program (Arc) but builds a fresh cache pool (meta/regex.rs impl Clone for Regex), so each thread owns its pool. It's thread-local rather than a leaked &'static clone, so long-lived processes that churn threads don't accumulate lazy-DFA caches. The 23 call sites change mechanically: re.is_match(input) becomes re.with(|r| r.is_match(input)), and likewise for find/captures, whose results borrow only the haystack.

Benchmarks

qsv stats on NYC 311 (1M rows, 539 MB), 16 jobs, M4 Max, release build. Outputs are byte-identical before/after across --infer-dates, --everything --infer-dates, --prefer-dmy --jobs 1 and --dates-whitelist all:

run before after
--infer-dates 266 ms 225 ms (1.18×)
--infer-dates --dates-whitelist all 193 ms 155 ms (1.25×)
--jobs 1 --infer-dates 1.70 s 1.71 s (unchanged)

Testing

  • cargo test, cargo fmt --check, cargo clippy --workspace --tests --all-features -D warnings: all pass
  • qsv's stats/schema/apply/validate/moarstats tests (1,040) pass with this branch patched in

🤖 Generated with Claude Code

`regex!` handed every caller the same global `Regex`. regex-automata gives
only the first thread to use a `Regex` the lock-free cache fast path; every
other thread goes through mutex-protected cache stacks. Under a parallel
caller that contention dominated date parsing: `qsv stats --infer-dates`
on 16 threads spent ~10% of busy CPU in pthread_mutex / Pool::get_slow /
Pool::put_value, and date inference cost +1.41 s CPU in parallel vs
+0.75 s single-threaded.

`regex!` now returns a `&'static LocalKey<Regex>` holding a per-thread
clone of the once-compiled regex. `Regex::clone` shares the compiled
program but builds a fresh cache pool, so each thread owns its own pool.
Thread-local rather than a leaked per-thread clone, so long-lived
processes that churn threads don't accumulate caches. Call sites change
mechanically from `re.is_match(input)` to `re.with(|r| r.is_match(input))`.

qsv stats on NYC 311 (1M rows, 16 jobs, M4 Max), outputs byte-identical:
- --infer-dates:                       266 ms -> 225 ms (1.18x)
- --infer-dates --dates-whitelist all: 193 ms -> 155 ms (1.25x)
- --jobs 1 --infer-dates:              1.70 s -> 1.71 s (unchanged)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jqnatividad
jqnatividad merged commit 3e46289 into main Oct 3, 2026
14 checks passed
@jqnatividad
jqnatividad deleted the thread-local-regex-pools branch October 3, 2026 19:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant