I've just refactored my Rust and C++ code which simulates the shoeshine shop model from this question. What else can be improved?
C++:
#include <iostream>
#include <iomanip>
#include <random>
#include <numeric>
#include <algorithm>
#include <array>
#include <queue>
#include <string>
#include <functional>
namespace shoeshine_shop {
enum event_t { ARRIVED, FIRST_FINISHED, SECOND_FINISHED };
enum state_t { EMPTY, FIRST, SECOND, WAITING, BOTH, DROP, INVALID };
using distribution_t = std::function<double()>;
struct pair_t {
double time;
event_t event;
};
struct stats_t {
std::array<size_t, DROP> state_counts;
std::array<size_t, DROP> state_counts_with_drop;
std::array<double, DROP> time_in_state;
std::array<double, 3> time_in_client;
double served_time;
size_t served_clients, arrived_clients, dropped_clients;
};
class simulation_t {
private:
template <typename T>
auto print_tables(std::string_view title, T const &counts) {
std::cout << title << "\n";
auto events = std::accumulate(std::begin(counts), std::end(counts), 0.0);
for (size_t i = 0; i < DROP; ++i)
std::cout << state_names[i] << ": " << counts[i] / double(events)
<< std::endl;
std::cout << std::endl;
}
void debug_view(pair_t &event, state_t &state) {
std::cout << "\t"
<< std::setprecision(std::numeric_limits<double>::digits10 + 1)
<< event.time << ": [" << state_names[state] << "] "
<< event_names[event.event] << " ==> ["
<< state_names[event_to_state[event.event][state]] << "]"
<< std::endl;
}
public:
simulation_t(distribution_t arrival, distribution_t first_serving,
distribution_t second_serving, std::uint64_t iterations = 50)
: window{queue_comparator}, iterations{1'000'000 * iterations},
distributions{arrival, first_serving, second_serving}, log_tail{0} {}
auto set_tail(std::uint64_t new_tail) noexcept { log_tail = new_tail; }
bool simulate() {
static auto pusher = [&](double t, event_t event) {
double dt = distributions[event]();
window.push({t + dt, event});
};
state_t state = EMPTY;
double prev = 0.0;
std::queue<double> arriving_times;
window.push({0.0, ARRIVED});
for (std::uint64_t i = 0; i < iterations; ++i) {
auto event = window.top();
window.pop();
if (iterations - i < log_tail)
debug_view(event, state);
switch (event.event) {
case ARRIVED:
++statistics.arrived_clients;
pusher(event.time, ARRIVED);
break;
case SECOND_FINISHED:
statistics.served_time += event.time - arriving_times.front();
arriving_times.pop();
++statistics.served_clients;
}
state_t new_state = event_to_state[event.event][state];
switch (new_state) {
case INVALID:
return false;
case DROP:
++statistics.state_counts_with_drop[state];
++statistics.dropped_clients;
continue;
case FIRST:
case BOTH:
if (event.event == ARRIVED) {
arriving_times.push(event.time);
pusher(event.time, FIRST_FINISHED);
}
break;
case SECOND:
pusher(event.time, SECOND_FINISHED);
break;
case EMPTY:
case WAITING:
break;
}
statistics.time_in_state[state] += event.time - prev;
statistics.time_in_client[state_to_clients[state]] += event.time - prev;
prev = event.time;
state = new_state;
++statistics.state_counts[state];
}
return true;
}
void print_report() {
std::transform(std::begin(statistics.state_counts),
std::end(statistics.state_counts),
std::begin(statistics.state_counts_with_drop),
std::begin(statistics.state_counts_with_drop),
std::plus<std::size_t>());
print_tables("time in states: ", statistics.time_in_state);
print_tables("entries in states: ", statistics.state_counts);
print_tables("entries in states with dropouts: ",
statistics.state_counts_with_drop);
std::cout << "dropout: "
<< statistics.dropped_clients / double(statistics.arrived_clients)
<< std::endl;
std::cout << "average serving time: "
<< statistics.served_time / double(statistics.served_clients)
<< std::endl;
std::cout << "average number of clients: "
<< (statistics.time_in_client[1] +
2 * statistics.time_in_client[2]) /
std::accumulate(std::begin(statistics.time_in_client),
std::end(statistics.time_in_client), 0.0)
<< std::endl;
}
private:
static constexpr std::array<const char *, 3> event_names{
{"ARRIVED", "FIRST_FINISHED", "SECOND_FINISHED"}};
static constexpr std::array<const char *, 7> state_names{
{"EMPTY", "FIRST", "SECOND", "WAITING", "BOTH", "DROP", "INVALID"}};
// clang-format off
static constexpr std::array<std::array<state_t, 5>, 3> event_to_state{
// EMPTY FIRST SECOND WAITING BOTH
/* ARRIVED */ {{FIRST, DROP, BOTH, DROP, DROP},
/* FIRST_FINISHED */ {INVALID, SECOND, INVALID, INVALID, WAITING},
/* SECOND_FINISHED */ {INVALID, INVALID, EMPTY, SECOND, FIRST}}};
// clang-format on
static constexpr std::array<size_t, DROP> state_to_clients{0, 1, 1, 2, 2};
inline static const auto queue_comparator = [](pair_t const &left,
pair_t const &right) {
return (left.time > right.time);
};
stats_t statistics{};
std::priority_queue<pair_t, std::vector<pair_t>, decltype(queue_comparator)>
window;
std::uint64_t iterations;
std::array<distribution_t, 3> distributions;
std::uint64_t log_tail;
};
} // namespace shoeshine_shop
int main(int argc, char **argv) {
if (argc < 5) {
std::cerr
<< "not enough arguments!\nlambda, m1, m2, millions of iterations";
return EXIT_FAILURE;
}
std::uint32_t seed = std::random_device{}();
std::mt19937 gen(seed);
std::exponential_distribution arrival(std::atof(argv[1]));
std::exponential_distribution first_serving(std::atof(argv[2]));
std::exponential_distribution second_serving(std::atof(argv[3]));
shoeshine_shop::simulation_t simul(std::bind(arrival, std::ref(gen)),
std::bind(first_serving, std::ref(gen)),
std::bind(second_serving, std::ref(gen)),
std::atoll(argv[4]));
if (argc == 6) {
seed = std::atol(argv[5]);
gen.seed(seed);
simul.set_tail(100);
}
if (!simul.simulate()) {
std::cerr << "ERROR: INVALID STATE REACHED, SEED: " << seed << std::endl;
return EXIT_FAILURE;
}
simul.print_report();
return EXIT_SUCCESS;
}
Rust:
use rand::{rngs::StdRng, Rng, SeedableRng};
use rand_distr::Exp;
use structopt::StructOpt;
mod shoeshine_shop {
use std::cmp::Reverse;
use std::collections::{BinaryHeap, VecDeque};
use std::convert::TryInto;
use ordered_float::*;
use rand::rngs::StdRng;
use rand_distr::Distribution;
#[derive(Copy, Clone, Debug, PartialEq, Ord, Eq, PartialOrd, enum_utils::TryFromRepr)]
#[repr(usize)]
enum Event {
Arrived = 0,
FirstFinished,
SecondFinished,
}
#[derive(Copy, Clone, Debug, PartialEq, Ord, Eq, PartialOrd, enum_utils::TryFromRepr)]
#[repr(usize)]
enum State {
Empty = 0,
First,
Second,
Waiting,
Both,
Dropping,
Invalid,
}
#[derive(Copy, Clone, Debug, Ord, Eq, PartialEq, PartialOrd)]
struct Pair {
time: OrderedFloat<f64>,
event: Event,
}
#[rustfmt::skip]
#[derive(Debug, Default)]
struct Stats {
state_counts: [u32; State::Dropping as usize],
state_counts_with_drop: [u32; State::Dropping as usize],
time_in_state: [f64; State::Dropping as usize],
time_in_client: [f64; 3],
served_time: f64,
served_clients: u32,
arrived_clients: u32,
dropped_clients: u32,
}
const STATE_TO_CLIENTS: [usize; State::Dropping as usize] = [0, 1, 1, 2, 2];
#[rustfmt::skip]
const EVENT_TO_STATE: [[State; 5]; 3] = [
// EMPTY FIRST SECOND WAITING BOTH
/* Arrived */ [First, Dropping, Both, Dropping, Dropping],
/* First_Finished */ [Invalid, Second, Invalid, Invalid, Waiting],
/* Second_Finished */ [Invalid, Invalid, Empty, Second, First],
];
macro_rules! report {
($title:expr, $counts:expr) => {{
println!("{}", $title);
let events: f64 = $counts.iter().copied().map(Into::<f64>::into).sum();
for (i, count) in $counts.iter().enumerate() {
let state: State = i.try_into().unwrap();
println!("{:?}: {}", state, Into::<f64>::into(*count) / events);
}
println!();
}};
}
pub struct Simulation<T: Distribution<f64>> {
statistics: Stats,
window: BinaryHeap<Reverse<Pair>>,
iterations: u64,
distributions: [T; 3],
log_tail: u64,
}
use Event::*;
use State::*;
impl<T> Simulation<T>
where
T: Distribution<f64>,
{
pub fn new(
arrival: T,
first_serving: T,
second_serving: T,
iterations: u64,
) -> Simulation<T> {
Simulation {
statistics: Stats::default(),
window: BinaryHeap::new(),
iterations: iterations,
distributions: [arrival, first_serving, second_serving],
log_tail: 0,
}
}
pub fn set_tail(&mut self, new_tail: u64) {
self.log_tail = new_tail;
}
pub fn print_report(&mut self) {
for (i, element) in self
.statistics
.state_counts_with_drop
.iter_mut()
.enumerate()
{
*element += self.statistics.state_counts[i];
}
report!("\ntime in states: ", self.statistics.time_in_state);
report!("entries in states: ", self.statistics.state_counts);
report!(
"entries in states with dropouts: ",
self.statistics.state_counts_with_drop
);
println!(
"dropout: {}\naverage serving time: {}\naverage number of clients: {}",
(self.statistics.dropped_clients as f64) / (self.statistics.arrived_clients as f64),
self.statistics.served_time / (self.statistics.served_clients as f64),
(self.statistics.time_in_client[1] + 2.0f64 * self.statistics.time_in_client[2])
/ self.statistics.time_in_client.iter().sum::<f64>()
);
}
pub fn simulate(&mut self, prng: &mut StdRng) -> bool {
macro_rules! pusher {
($t:expr, $event:expr) => {{
let dt: f64 = self.distributions[$event as usize].sample(prng).into();
self.window.push(Reverse(Pair {
time: ($t + dt).into(),
event: $event,
}));
}};
}
let mut prev = 0f64;
let mut state = State::Empty;
let mut arriving_times = VecDeque::<f64>::new();
self.window.push(Reverse(Pair {
time: 0.0.into(),
event: Arrived,
}));
for i in 0..self.iterations {
let event = self.window.pop().unwrap().0;
if self.iterations - i < self.log_tail {
println!(
"{}: [{:?}] {:?} ==> [{:?}]",
event.time.0,
state,
event.event,
EVENT_TO_STATE[event.event as usize][state as usize]
);
}
match event.event {
Arrived => {
self.statistics.arrived_clients += 1;
pusher!(event.time.0, Arrived);
}
SecondFinished => {
self.statistics.served_time +=
event.time.0 - arriving_times.front().unwrap();
arriving_times.pop_front();
self.statistics.served_clients += 1;
}
_ => (),
}
let new_state = EVENT_TO_STATE[event.event as usize][state as usize];
match new_state {
Invalid => return false,
Dropping => {
self.statistics.state_counts_with_drop[state as usize] += 1;
self.statistics.dropped_clients += 1;
continue;
}
First | Both if event.event == Arrived => {
arriving_times.push_back(event.time.0);
pusher!(event.time.0, FirstFinished);
}
Second => pusher!(event.time.0, SecondFinished),
_ => (),
}
self.statistics.time_in_state[state as usize] += event.time.0 - prev;
self.statistics.time_in_client[STATE_TO_CLIENTS[state as usize]] +=
event.time.0 - prev;
prev = event.time.0;
state = new_state;
self.statistics.state_counts[state as usize] += 1;
}
true
}
}
}
///shoe shine shop simulation
///
/// Shoe shine shop has two chairs, one for brushing (1) and another for polishing (2).
/// Customers arrive according to PP with rate λ, and enter only if first chair is empty.
/// Shoe-shiners takes exp(μ1) time for brushing and exp(μ2) time for polishing.
#[derive(StructOpt, Debug)]
#[structopt(name = "sim")]
struct Args {
///rate of customer arrival
#[structopt(long)]
lambda: f64,
///rate of serving on the first chair
#[structopt(long)]
mu1: f64,
///rate of serving on the second chair
#[structopt(long)]
mu2: f64,
///millions of events to simulate
#[structopt(short)]
iterations: u64,
///expilictly set seed
#[structopt(short)]
seed: Option<u64>,
///change log tail
#[structopt(short, default_value = "0")]
tail: u64,
}
fn main() {
let args = Args::from_args();
let mut simulation = shoeshine_shop::Simulation::new(
Exp::new(args.lambda).unwrap(),
Exp::new(args.mu1).unwrap(),
Exp::new(args.mu2).unwrap(),
args.iterations * 1_000_000,
);
let seed = args.seed.unwrap_or(rand::thread_rng().gen());
simulation.set_tail(args.tail);
let mut prng: StdRng = SeedableRng::seed_from_u64(seed);
if !simulation.simulate(&mut prng) {
panic!("Error: invalid state reached, seed: {}", seed);
}
simulation.print_report();
}