Skip to content

Commit c21ed55

Browse files
committed
automata: make MAX_POOL_STACKS user configurable
This exposes a knob inside of `regex-automata` (but not `regex`) that permits end users to control how many cache lines are used inside of the cache pool. Previously, this was always fixed to `8`, but using the benchmark in #934 shows that this can cause performance to suffer when the number of threads is greater than `8`. In this commit, we not only expose it as user configurable, but we also change the default to be `std::thread::available_parallelism()`. This does increase memory usage for environments with higher core counts, but it's still probably the better default. If this proves burdensome, we can always roll the default back to `8`. Fixes #1241
1 parent 2b52759 commit c21ed55

2 files changed

Lines changed: 107 additions & 3 deletions

File tree

‎regex-automata/src/meta/regex.rs‎

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1919,7 +1919,10 @@ impl Clone for Regex {
19191919
let pool = {
19201920
let strat = Arc::clone(&imp.strat);
19211921
let create: CachePoolFn = Box::new(move || strat.create_cache());
1922-
Pool::new(create)
1922+
Pool::with_capacity(
1923+
self.imp.info.config().get_pool_capacity(),
1924+
create,
1925+
)
19231926
};
19241927
Regex { imp, pool }
19251928
}
@@ -2483,6 +2486,7 @@ pub struct Config {
24832486
backtrack: Option<bool>,
24842487
byte_classes: Option<bool>,
24852488
line_terminator: Option<u8>,
2489+
pool_capacity: Option<usize>,
24862490
}
24872491

24882492
impl Config {
@@ -3039,6 +3043,17 @@ impl Config {
30393043
Config { line_terminator: Some(byte), ..self }
30403044
}
30413045

3046+
/// Sets the capacity used to manage a pool of [`Cache`] values in the
3047+
/// higher level convenience APIs.
3048+
///
3049+
/// When not configured explicitly, a reasonable default is selected. It
3050+
/// is rarely expected that a number large than the number of logical CPUs
3051+
/// makes sense as a value. A smaller number could result in slowdowns if
3052+
/// many regex queries are run under contention.
3053+
pub fn pool_capacity(self, capacity: usize) -> Config {
3054+
Config { pool_capacity: Some(capacity), ..self }
3055+
}
3056+
30423057
/// Toggle whether the hybrid NFA/DFA (also known as the "lazy DFA") should
30433058
/// be available for use by the meta regex engine.
30443059
///
@@ -3210,6 +3225,30 @@ impl Config {
32103225
self.line_terminator.unwrap_or(b'\n')
32113226
}
32123227

3228+
/// Returns the configured pool capacity, as set by
3229+
/// [`Config::pool_capacity`].
3230+
///
3231+
/// If it was not explicitly set, then a default value is returned.
3232+
pub fn get_pool_capacity(&self) -> usize {
3233+
// The default is an empirically chosen value that balances memory
3234+
// usage with runtime performance. In practice, with `std` enabled,
3235+
// we choose a value that matches the total number of CPUs.
3236+
const DEFAULT_POOL_CAPACITY: usize = 8;
3237+
3238+
self.pool_capacity.unwrap_or_else(|| {
3239+
#[cfg(feature = "std")]
3240+
{
3241+
std::thread::available_parallelism()
3242+
.map(|n| n.get())
3243+
.unwrap_or(DEFAULT_POOL_CAPACITY)
3244+
}
3245+
#[cfg(not(feature = "std"))]
3246+
{
3247+
DEFAULT_POOL_CAPACITY
3248+
}
3249+
})
3250+
}
3251+
32133252
/// Returns whether the hybrid NFA/DFA regex engine may be used, as set by
32143253
/// [`Config::hybrid`].
32153254
///
@@ -3317,6 +3356,7 @@ impl Config {
33173356
backtrack: o.backtrack.or(self.backtrack),
33183357
byte_classes: o.byte_classes.or(self.byte_classes),
33193358
line_terminator: o.line_terminator.or(self.line_terminator),
3359+
pool_capacity: o.pool_capacity.or(self.pool_capacity),
33203360
}
33213361
}
33223362
}
@@ -3641,7 +3681,7 @@ impl Builder {
36413681
let pool = {
36423682
let strat = Arc::clone(&strat);
36433683
let create: CachePoolFn = Box::new(move || strat.create_cache());
3644-
Pool::new(create)
3684+
Pool::with_capacity(self.config.get_pool_capacity(), create)
36453685
};
36463686
Ok(Regex { imp: Arc::new(RegexI { strat, info }), pool })
36473687
}

‎regex-automata/src/util/pool.rs‎

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,50 @@ impl<T, F> Pool<T, F> {
159159
pub fn new(create: F) -> Pool<T, F> {
160160
Pool(alloc::boxed::Box::new(inner::Pool::new(create)))
161161
}
162+
163+
/// Create a new pool. The given closure is used to create values in
164+
/// the pool when necessary.
165+
///
166+
/// When the `std` feature is enabled, a `Pool` is thread-aware and spreads
167+
/// its memory out across multiple cache lines. The number of cache lines
168+
/// is determined by the `capacity` parameter passed here. By default, a
169+
/// fixed reasonable number is used. A smaller number means less memory is
170+
/// used, but a higher number means there may be less contention on this
171+
/// pool in highly threaded environments doing a lot of searches using the
172+
/// same `Regex` value.
173+
///
174+
/// When `std` is not enabled, then the capacity parameter is ignored
175+
/// because the underlying pool implementation is not thread-aware.
176+
///
177+
/// The capacity must be at least 1. If it's less than 1, then it is
178+
/// forced to be 1.
179+
pub fn with_capacity(capacity: usize, create: F) -> Pool<T, F> {
180+
Pool(alloc::boxed::Box::new(inner::Pool::with_capacity(
181+
capacity, create,
182+
)))
183+
}
184+
185+
/// Create a new pool. The given closure is used to create values in
186+
/// the pool when necessary.
187+
///
188+
/// This is a convenience routine for calling `Pool::with_capacity` with
189+
/// a number equivalent to the available parallelism for this environment.
190+
///
191+
/// If `std` is not enabled or if the query for available parallelism
192+
/// failed, then this is equivalent to calling `Pool::new`.
193+
pub fn with_available_parallelism_capacity(create: F) -> Pool<T, F> {
194+
#[cfg(feature = "std")]
195+
{
196+
let Ok(n) = std::thread::available_parallelism() else {
197+
return Pool::new(create);
198+
};
199+
Pool::with_capacity(n.get(), create)
200+
}
201+
#[cfg(not(feature = "std"))]
202+
{
203+
Pool::new(create)
204+
}
205+
}
162206
}
163207

164208
impl<T: Send, F: Fn() -> T> Pool<T, F> {
@@ -455,6 +499,18 @@ mod inner {
455499
/// Create a new pool. The given closure is used to create values in
456500
/// the pool when necessary.
457501
pub(super) fn new(create: F) -> Pool<T, F> {
502+
Pool::with_capacity(MAX_POOL_STACKS, create)
503+
}
504+
505+
/// Create a new pool. The given closure is used to create values in
506+
/// the pool when necessary.
507+
///
508+
/// The given capacity is used to determine how many cache lines to
509+
/// maintain. Each cache line contains a stack of cached entries.
510+
///
511+
/// The capacity must be at least 1. If it's less than 1, then it is
512+
/// forced to be 1.
513+
pub(super) fn with_capacity(capacity: usize, create: F) -> Pool<T, F> {
458514
// FIXME: Now that we require 1.65+, Mutex::new is available as
459515
// const... So we can almost mark this function as const. But of
460516
// course, we're creating a Vec of stacks below (we didn't when I
@@ -493,7 +549,7 @@ mod inner {
493549
// Back to square one. I maybe we just don't make a pool's
494550
// constructor const and live with it. It's probably not a huge
495551
// deal.
496-
let mut stacks = Vec::with_capacity(MAX_POOL_STACKS);
552+
let mut stacks = Vec::with_capacity(capacity.max(1));
497553
for _ in 0..stacks.capacity() {
498554
stacks.push(CacheLine(Mutex::new(vec![])));
499555
}
@@ -847,6 +903,14 @@ mod inner {
847903
pub(super) const fn new(create: F) -> Pool<T, F> {
848904
Pool { stack: Mutex::new(vec![]), create }
849905
}
906+
907+
/// This is a no-op since this pool implementation isn't thread-aware.
908+
pub(super) const fn with_capacity(
909+
_capacity: usize,
910+
create: F,
911+
) -> Pool<T, F> {
912+
Pool::new(create)
913+
}
850914
}
851915

852916
impl<T: Send, F: Fn() -> T> Pool<T, F> {

0 commit comments

Comments
 (0)