use std::{
cell::RefCell,
collections::{HashMap, VecDeque},
num::NonZero,
sync::{
Condvar, Mutex, MutexGuard, Once, PoisonError,
atomic::{AtomicBool, AtomicU64, Ordering}
},
thread::available_parallelism
};
use rayon::{
Scope, ThreadPoolBuilder, broadcast, current_num_threads,
iter::{
ParallelIterator,
plumbing::{Folder, Reducer, UnindexedConsumer}
},
scope
};
use super::{CanBuildHistogram, EvaluationState, Histogram, Meter};
use crate::{EvaluationError, Evaluator, Function};
impl Histogram
{
fn merge(&mut self, other: Self)
{
for (key, value) in other.into_iter()
{
*self.entry(key).or_insert(0) += value;
}
}
}
#[derive(Debug, Clone)]
pub struct HistogramBuilder
{
function: Function,
environment: HashMap<usize, i32>,
generation: u64
}
static THREAD_POOL_INITIALIZER: Once = Once::new();
impl HistogramBuilder
{
pub fn use_available_parallelism()
{
THREAD_POOL_INITIALIZER.call_once(|| {
let _ = ThreadPoolBuilder::new()
.num_threads(available_parallelism().map_or(1, NonZero::get))
.build_global();
});
}
pub fn set_parallelism(&self, num_threads: usize)
{
THREAD_POOL_INITIALIZER.call_once(|| {
let _ = ThreadPoolBuilder::new()
.num_threads(num_threads)
.build_global();
});
}
}
fn increment(generation: u64, outcome: i32)
{
HISTOGRAM.with_borrow_mut(|histogram| {
*histogram
.entry(generation)
.or_default()
.entry(outcome)
.or_insert(0) += 1;
});
}
fn finish(generation: u64) -> Histogram
{
let partials = broadcast(|_| {
HISTOGRAM
.with_borrow_mut(|histogram| histogram.remove(&generation))
.unwrap_or_default()
});
partials
.into_iter()
.reduce(|mut a, b| {
a.merge(b);
a
})
.unwrap()
}
impl<'inst> super::HistogramBuilder<'inst, EvaluationStateIterator<'inst>>
for HistogramBuilder
{
fn new(evaluator: Evaluator) -> Self
{
HistogramBuilder {
function: evaluator.function,
environment: evaluator.environment,
generation: GENERATION.fetch_add(1, Ordering::Relaxed)
}
}
fn build_metered(
&self,
args: impl IntoIterator<Item = i32> + Send,
budget: u64
) -> Result<Histogram, EvaluationError<'static>>
{
let generation = self.generation;
let meter = Meter::new(budget);
let histogram = scope(|_| {
CanBuildHistogram::iter(self, args, meter.limited())?
.flat_map(|state| state.result)
.for_each(|outcome| increment(generation, outcome));
Ok::<_, EvaluationError<'static>>(finish(generation))
})?;
meter.verdict()?;
Ok(histogram)
}
fn build_while(
&self,
args: impl IntoIterator<Item = i32> + Send,
condition: impl Fn(&i32) -> bool + Send + Sync
) -> Result<Histogram, EvaluationError<'_>>
{
let generation = self.generation;
scope(|_| {
CanBuildHistogram::iter(self, args, None)?
.flat_map(|state| state.result)
.take_any_while(condition)
.for_each(|outcome| increment(generation, outcome));
Ok(finish(generation))
})
}
#[inline]
fn iter(
&'inst self,
args: impl IntoIterator<Item = i32>
) -> Result<EvaluationStateIterator<'inst>, EvaluationError<'inst>>
{
CanBuildHistogram::iter(self, args, None)
}
}
impl<'inst> CanBuildHistogram<'inst, EvaluationStateIterator<'inst>>
for HistogramBuilder
{
#[inline]
fn function(&self) -> &Function { &self.function }
#[inline]
fn environment(&self) -> &HashMap<usize, i32> { &self.environment }
fn create_iterator(
&'inst self,
initial_state: EvaluationState<'inst>,
meter: Option<&'inst Meter>
) -> EvaluationStateIterator<'inst>
{
HistogramBuilder::use_available_parallelism();
EvaluationStateIterator {
states: [initial_state].into(),
meter
}
}
}
static GENERATION: AtomicU64 = AtomicU64::new(0);
thread_local! {
pub static HISTOGRAM: RefCell<HashMap<u64, Histogram>> =
RefCell::new(Default::default());
}
#[derive(Debug, Clone)]
pub struct EvaluationStateIterator<'inst>
{
states: VecDeque<EvaluationState<'inst>>,
meter: Option<&'inst Meter>
}
impl<'inst> ParallelIterator for EvaluationStateIterator<'inst>
{
type Item = EvaluationState<'inst>;
fn drive_unindexed<C>(self, consumer: C) -> C::Result
where
C: UnindexedConsumer<Self::Item>
{
let workers = current_num_threads().max(1);
let mut consumers = Vec::with_capacity(workers);
let mut reducers = Vec::with_capacity(workers - 1);
for _ in 1..workers
{
reducers.push(consumer.to_reducer());
consumers.push(consumer.split_off_left());
}
consumers.push(consumer);
let mut consumers = consumers.into_iter().enumerate();
let (_, first) = consumers.next().unwrap();
let crew = Crew::new(self.states, consumers.collect(), self.meter);
scope(|scope| crew.work(scope, 0, first));
let mut results = crew
.results
.into_iter()
.map(|result| result.into_inner().unwrap())
.collect::<Vec<_>>();
for (index, consumer) in crew.reserve.into_inner().unwrap()
{
results[index] = Some(consumer.into_folder().complete());
}
let mut results = results.into_iter().map(Option::unwrap);
let mut result = results.next_back().unwrap();
for (reducer, left) in reducers.into_iter().rev().zip(results.rev())
{
result = reducer.reduce(left, result);
}
result
}
}
#[cfg_attr(doc, aquamarine::aquamarine)]
struct Crew<'inst, C>
where
C: UnindexedConsumer<EvaluationState<'inst>>
{
queue: WorkQueue<'inst>,
reserve: Mutex<Vec<(usize, C)>>,
recruiting: AtomicBool,
results: Vec<Mutex<Option<C::Result>>>,
meter: Option<&'inst Meter>
}
struct WorkQueue<'inst>
{
shared: Mutex<SharedWork<'inst>>,
changed: Condvar,
hungry: AtomicBool
}
struct SharedWork<'inst>
{
states: VecDeque<EvaluationState<'inst>>,
busy: usize
}
struct Shift<'queue, 'inst>
{
queue: &'queue WorkQueue<'inst>,
stack: Vec<EvaluationState<'inst>>
}
impl<'inst, C> Crew<'inst, C>
where
C: UnindexedConsumer<EvaluationState<'inst>>
{
const RECRUITMENT_INTERVAL: usize = 64;
fn new(
states: VecDeque<EvaluationState<'inst>>,
reserve: Vec<(usize, C)>,
meter: Option<&'inst Meter>
) -> Self
{
let workers = reserve.len() + 1;
Crew {
queue: WorkQueue::new(states),
recruiting: AtomicBool::new(!reserve.is_empty()),
reserve: Mutex::new(reserve),
results: (0..workers).map(|_| Mutex::new(None)).collect(),
meter
}
}
#[inline]
fn must_stop(&self, folder: &C::Folder) -> bool
{
folder.full() || self.meter.is_some_and(Meter::refused)
}
fn work<'scope>(
&'scope self,
scope: &Scope<'scope>,
index: usize,
consumer: C
) where
'inst: 'scope,
C: 'scope
{
let mut folder = consumer.into_folder();
let mut evaluated = 0;
while !self.must_stop(&folder)
{
let Some(mut shift) = self.queue.acquire()
else
{
break
};
while !self.must_stop(&folder)
{
let Some(mut state) = shift.stack.pop()
else
{
break
};
let outcome = state.evaluate();
if let Some(successors) = state.successors.take()
{
shift.stack.extend(successors.into_iter().rev());
}
if outcome.is_some()
{
folder = folder.consume(state);
}
evaluated += 1;
if evaluated >= Self::RECRUITMENT_INTERVAL
&& shift.stack.len() > 1
&& self.recruiting.load(Ordering::Relaxed)
{
evaluated = 0;
self.recruit(scope, &mut shift.stack);
}
else if shift.stack.len() > 1
&& self.queue.hungry.load(Ordering::Relaxed)
{
self.queue.share(&mut shift.stack);
}
}
}
*self.results[index].lock().unwrap() = Some(folder.complete());
}
fn recruit<'scope>(
&'scope self,
scope: &Scope<'scope>,
stack: &mut Vec<EvaluationState<'inst>>
) where
'inst: 'scope,
C: 'scope
{
let mut reserve = self.reserve.lock().unwrap();
let recruit = reserve.pop();
if reserve.is_empty()
{
self.recruiting.store(false, Ordering::Relaxed);
}
drop(reserve);
if let Some((index, consumer)) = recruit
{
self.queue.share(stack);
scope.spawn(move |scope| self.work(scope, index, consumer));
}
}
}
impl<'inst> WorkQueue<'inst>
{
fn new(states: VecDeque<EvaluationState<'inst>>) -> Self
{
WorkQueue {
shared: Mutex::new(SharedWork { states, busy: 0 }),
changed: Condvar::new(),
hungry: AtomicBool::new(false)
}
}
fn lock(&self) -> MutexGuard<'_, SharedWork<'inst>>
{
self.shared.lock().unwrap_or_else(PoisonError::into_inner)
}
fn acquire(&self) -> Option<Shift<'_, 'inst>>
{
let mut shared = self.lock();
loop
{
let queued = shared.states.len();
if queued > 0
{
let stack = shared
.states
.drain(..queued.div_ceil(2))
.collect::<Vec<_>>();
shared.busy += 1;
return Some(Shift { queue: self, stack })
}
if shared.busy == 0
{
return None
}
self.hungry.store(true, Ordering::Relaxed);
shared = self
.changed
.wait(shared)
.unwrap_or_else(PoisonError::into_inner);
}
}
fn share(&self, stack: &mut Vec<EvaluationState<'inst>>)
{
let mut shared = self.lock();
shared.states.extend(stack.drain(..stack.len() / 2));
self.hungry.store(false, Ordering::Relaxed);
drop(shared);
self.changed.notify_all();
}
}
impl Drop for Shift<'_, '_>
{
fn drop(&mut self)
{
let mut shared = self.queue.lock();
shared.states.extend(self.stack.drain(..));
shared.busy -= 1;
drop(shared);
self.queue.changed.notify_all();
}
}