Optimized 2022/day19-part1, with a lot of potential still left

I wonder if the code I added actually improves or worsens performance.
It doesn't feel like I'm making big progress, despite all the time
spent.
This commit is contained in:
Tobias Marschner 2024-04-09 15:11:45 +02:00
parent d71d669eb3
commit 695d262a58

View File

@ -1,7 +1,4 @@
use std::{
cmp::Ordering,
collections::{HashSet, VecDeque},
};
use std::{cmp::Ordering, collections::HashSet};
#[derive(Debug)]
struct Blueprint {
@ -125,22 +122,71 @@ impl Blueprint {
println!(" inserted elements: {}", insert_count);
println!(" initial next_hs.len(): {}", next_hs.len());
if next_hs.len() < 50_000 && ts > 4 {
// For the last few timesteps, pruning isn't worth it. Just calculate the rest in that case.
if ts > 3 {
// Pruning runs in a nested loop over the elements of the set.
// In the inner loop we collect strictly inferior elements until we've met a
// threshold of the original count.
let initial_elems = next_hs.len() as f32;
// If the inner loop has found strictly inferior elements making up 10% of the
// original count, exit out of it and actually remove them before continuing to
// prune.
const INNER_LOOP_CUTOFF: f32 = 0.1;
// Count the total number of comparisons performed.
let mut comp_count = 0usize;
// This is the cutoff. Once we've reached this number of comparisons, we quit.
const TOTAL_COMP_COUNT: usize = 400_000_000usize;
// Next up: Reduce the number of elements in the set using the Pareto optimality comparsion.
println!("Removing redundant elements ...");
let mut reduced_hs: HashSet<RecursionState> = HashSet::new();
for rs in &next_hs {
// Carry over rs if it is part of the HS's Pareto frontier.
if next_hs.iter().all(|e| rs.compare(e).is_ge()) {
reduced_hs.insert(*rs);
println!("Pruning redundant elements ...");
loop {
let mut strictly_inferior_hs: HashSet<RecursionState> = HashSet::new();
for ars in &next_hs {
// Run a nested loop and mark any strictly inferior elements.
// Also keep track of whether ars may be part of the frontier.
for brs in &next_hs {
comp_count += 1;
match ars.compare(brs) {
Ordering::Greater => {
strictly_inferior_hs.insert(*brs);
}
Ordering::Less => {
strictly_inferior_hs.insert(*ars);
}
Ordering::Equal => (),
}
}
// Have we found enough elements yet?
// Or have we reached the limit in terms of comparisons?
if (strictly_inferior_hs.len() as f32) / initial_elems >= INNER_LOOP_CUTOFF
|| comp_count >= TOTAL_COMP_COUNT
{
break;
}
}
// No more inferior elements? Then we're done.
if strictly_inferior_hs.is_empty() {
println!("No more inferior elements. Fully pruned.");
println!("comp_count: {}", comp_count);
break;
} else {
// Actually remove the strictly inferior elements from next_hs.
next_hs = next_hs
.difference(&strictly_inferior_hs)
.cloned()
.collect::<HashSet<_>>();
// And reset the strictly_inferior set.
strictly_inferior_hs.clear();
// If we've hit the limit in terms of no. of comparsions, just move on.
if comp_count >= TOTAL_COMP_COUNT {
println!("Comparison limit reached. Continuing.");
break;
}
}
}
next_hs = reduced_hs;
println!(" done!");
} else {
println!("Number of states too large or timeslot too late. Not optimizing.");
println!("Final 3 - no more pruning.");
}
println!(" final next_hs.len(): {}\n", next_hs.len());
}
@ -195,37 +241,82 @@ impl RecursionState {
// that RS1 is "strictly better". It is different, having made a different tradeoff in
// resource collection, which may or may not lead to a better outcome overall.
// This comparison is used to cut off redundant simulation paths in the recursive solver.
#[allow(clippy::comparison_chain)]
fn compare(&self, other: &Self) -> Ordering {
// Collect the comparison result on all metrics.
// let mut metrics: Vec<Ordering> = Vec::with_capacity(9);
let mut metrics: [Ordering; 9] = [Ordering::Equal; 9];
// Resource counts
metrics[0] = self.ore.cmp(&other.ore);
metrics[1] = self.clay.cmp(&other.clay);
metrics[2] = self.obsidian.cmp(&other.obsidian);
metrics[3] = self.geode.cmp(&other.geode);
// Robot counts
metrics[4] = self.ore_robots.cmp(&other.ore_robots);
metrics[5] = self.clay_robots.cmp(&other.clay_robots);
metrics[6] = self.obsidian_robots.cmp(&other.obsidian_robots);
metrics[7] = self.geode_robots.cmp(&other.geode_robots);
// Remaining time
metrics[8] = self.remaining_time.cmp(&other.remaining_time);
// This used to be a lot more idiomatic.
// However, to help improve performance, it now looks the way it does.
let mut less = false;
let mut greater = false;
// If one metric is strictly superior and the rest are better or equal,
// this RecursionState is "strictly better".
if metrics.iter().all(|e| e.is_ge()) && metrics.iter().any(|e| e.is_gt()) {
Ordering::Greater
// If one metric is strictly inferior and the rest are inferior or equal,
// this RecursionState is "strictly inferior".
} else if metrics.iter().all(|e| e.is_le()) && metrics.iter().any(|e| e.is_lt()) {
Ordering::Less
// In all other cases no definitive statement can be made.
} else {
if self.ore < other.ore {
less = true;
} else if self.ore > other.ore {
greater = true;
}
if self.clay < other.clay {
less = true;
} else if self.clay > other.clay {
greater = true;
}
if self.obsidian < other.obsidian {
less = true;
} else if self.obsidian > other.obsidian {
greater = true;
}
if self.geode < other.geode {
less = true;
} else if self.geode > other.geode {
greater = true;
}
// Short-circuit.
if less && greater {
return Ordering::Equal;
}
if self.ore_robots < other.ore_robots {
less = true;
} else if self.ore_robots > other.ore_robots {
greater = true;
}
if self.clay_robots < other.clay_robots {
less = true;
} else if self.clay_robots > other.clay_robots {
greater = true;
}
if self.obsidian_robots < other.obsidian_robots {
less = true;
} else if self.obsidian_robots > other.obsidian_robots {
greater = true;
}
if self.geode_robots < other.geode_robots {
less = true;
} else if self.geode_robots > other.geode_robots {
greater = true;
}
if self.remaining_time < other.remaining_time {
less = true;
} else if self.remaining_time > other.remaining_time {
greater = true;
}
if (!less && !greater) || (less && greater) {
Ordering::Equal
} else if less {
Ordering::Less
} else {
Ordering::Greater
}
}
#[allow(dead_code)]
fn print(&self) {
print!("{:>3} O, ", self.ore);
print!("{:>3} C, ", self.clay);