#![cfg(feature = "parallel")]
use crate::deriv::log::DerivedExpr;
use crate::kernel::{ExprData, ExprId, ExprPool};
use crate::simplify::engine::SimplifyConfig;
pub const WIDTH_THRESHOLD: f64 = 8.0;
pub const MIN_WORKERS_FOR_FORK_JOIN: usize = 8;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Strategy {
ForkJoin,
LevelScheduled,
}
pub fn simplify_auto(expr: ExprId, pool: &ExprPool) -> DerivedExpr<ExprId> {
simplify_auto_with_config(expr, pool, &SimplifyConfig::default())
}
pub fn simplify_auto_with_config(
expr: ExprId,
pool: &ExprPool,
config: &SimplifyConfig,
) -> DerivedExpr<ExprId> {
match choose_strategy(expr, pool) {
Strategy::ForkJoin => super::parallel::simplify_par_with_config(expr, pool, config),
Strategy::LevelScheduled => super::redex::simplify_redex_with_config(expr, pool, config),
}
}
pub fn choose_strategy(expr: ExprId, pool: &ExprPool) -> Strategy {
if rayon::current_num_threads() < MIN_WORKERS_FOR_FORK_JOIN {
return Strategy::LevelScheduled;
}
let (nodes, height) = shape(expr, pool);
let average_width = nodes as f64 / height.max(1) as f64;
if average_width >= WIDTH_THRESHOLD {
Strategy::ForkJoin
} else {
Strategy::LevelScheduled
}
}
fn shape(root: ExprId, pool: &ExprPool) -> (usize, u32) {
let n = pool.len();
let mut height = vec![u32::MAX; n];
let mut pushed = vec![false; n];
let mut stack: Vec<(ExprId, bool)> = vec![(root, false)];
let mut nodes = 0_usize;
let mut max_height = 0_u32;
while let Some((id, expanded)) = stack.pop() {
let i = id.0 as usize;
if expanded {
let h = pool.with(id, |data| {
let mut h = 0_u32;
for_each_child(data, |c| {
let ch = height[c.0 as usize];
debug_assert_ne!(ch, u32::MAX, "child measured after its parent");
h = h.max(ch.saturating_add(1));
});
h
});
height[i] = h;
max_height = max_height.max(h);
nodes += 1;
continue;
}
if pushed[i] {
continue;
}
pushed[i] = true;
stack.push((id, true));
pool.with(id, |data| {
for_each_child(data, |c| {
if !pushed[c.0 as usize] {
stack.push((c, false));
}
})
});
}
(nodes, max_height + 1)
}
fn for_each_child(data: &ExprData, mut f: impl FnMut(ExprId)) {
match data {
ExprData::Add(args) | ExprData::Mul(args) => args.iter().copied().for_each(f),
ExprData::Func { args, .. } | ExprData::Predicate { args, .. } => {
args.iter().copied().for_each(f)
}
ExprData::Pow { base, exp } => {
f(*base);
f(*exp);
}
ExprData::Piecewise { branches, default } => {
branches.iter().for_each(|&(_, v)| f(v));
f(*default);
}
ExprData::Forall { body, .. } | ExprData::Exists { body, .. } => f(*body),
ExprData::BigO(arg) => f(*arg),
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::kernel::Domain;
use crate::simplify::simplify;
fn p() -> ExprPool {
ExprPool::new()
}
fn junk(pool: &ExprPool, x: ExprId, depth: usize) -> ExprId {
let one = pool.integer(1_i32);
let zero = pool.integer(0_i32);
let mut e = x;
for _ in 0..depth {
e = pool.mul(vec![e, one]);
e = pool.add(vec![e, zero]);
}
e
}
fn with_workers<R: Send>(f: impl FnOnce() -> R + Send) -> R {
rayon::ThreadPoolBuilder::new()
.num_threads(MIN_WORKERS_FOR_FORK_JOIN)
.build()
.unwrap()
.install(f)
}
#[test]
fn deep_chain_picks_level_scheduling() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let deep = junk(&pool, x, 300);
assert_eq!(
with_workers(|| choose_strategy(deep, &pool)),
Strategy::LevelScheduled
);
}
#[test]
fn wide_sum_picks_fork_join() {
let pool = p();
let zero = pool.integer(0_i32);
let args: Vec<ExprId> = (0..256)
.map(|i| {
let x = pool.symbol(format!("x{i}"), Domain::Real);
pool.add(vec![x, zero])
})
.collect();
let wide = pool.add(args);
assert_eq!(
with_workers(|| choose_strategy(wide, &pool)),
Strategy::ForkJoin
);
}
#[test]
fn few_workers_pick_level_scheduling() {
let pool = p();
let zero = pool.integer(0_i32);
let args: Vec<ExprId> = (0..256)
.map(|i| {
let x = pool.symbol(format!("y{i}"), Domain::Real);
pool.add(vec![x, zero])
})
.collect();
let wide = pool.add(args);
let tp = rayon::ThreadPoolBuilder::new()
.num_threads(MIN_WORKERS_FOR_FORK_JOIN - 1)
.build()
.unwrap();
assert_eq!(
tp.install(|| choose_strategy(wide, &pool)),
Strategy::LevelScheduled
);
}
#[test]
fn auto_matches_sequential_on_both_shapes() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let deep = junk(&pool, x, 200);
let zero = pool.integer(0_i32);
let args: Vec<ExprId> = (0..64)
.map(|i| {
let s = pool.symbol(format!("z{i}"), Domain::Real);
junk(&pool, s, 4)
})
.collect();
let wide = pool.add(args);
for expr in [deep, wide] {
let seq = simplify(expr, &pool).value;
let auto = with_workers(|| simplify_auto(expr, &pool).value);
assert_eq!(seq, auto);
}
let _ = zero;
}
#[test]
fn shape_measures_width_and_height() {
let pool = p();
let x = pool.symbol("x", Domain::Real);
let deep = junk(&pool, x, 10);
let (nodes, height) = shape(deep, &pool);
assert!(
height >= 20,
"chain should be at least 20 levels, got {height}"
);
assert!(
(nodes as f64 / height as f64) < WIDTH_THRESHOLD,
"a chain must read as narrow"
);
}
}