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::{ use std::{cmp::Ordering, collections::HashSet};
cmp::Ordering,
collections::{HashSet, VecDeque},
};
#[derive(Debug)] #[derive(Debug)]
struct Blueprint { struct Blueprint {
@ -125,22 +122,71 @@ impl Blueprint {
println!(" inserted elements: {}", insert_count); println!(" inserted elements: {}", insert_count);
println!(" initial next_hs.len(): {}", next_hs.len()); 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. // Next up: Reduce the number of elements in the set using the Pareto optimality comparsion.
println!("Removing redundant elements ..."); println!("Pruning redundant elements ...");
let mut reduced_hs: HashSet<RecursionState> = HashSet::new(); loop {
for rs in &next_hs { let mut strictly_inferior_hs: HashSet<RecursionState> = HashSet::new();
// Carry over rs if it is part of the HS's Pareto frontier. for ars in &next_hs {
if next_hs.iter().all(|e| rs.compare(e).is_ge()) { // Run a nested loop and mark any strictly inferior elements.
reduced_hs.insert(*rs); // 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 { } 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()); 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 // 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. // 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. // This comparison is used to cut off redundant simulation paths in the recursive solver.
#[allow(clippy::comparison_chain)]
fn compare(&self, other: &Self) -> Ordering { fn compare(&self, other: &Self) -> Ordering {
// Collect the comparison result on all metrics. // This used to be a lot more idiomatic.
// let mut metrics: Vec<Ordering> = Vec::with_capacity(9); // However, to help improve performance, it now looks the way it does.
let mut metrics: [Ordering; 9] = [Ordering::Equal; 9]; let mut less = false;
// Resource counts let mut greater = false;
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);
// If one metric is strictly superior and the rest are better or equal, if self.ore < other.ore {
// this RecursionState is "strictly better". less = true;
if metrics.iter().all(|e| e.is_ge()) && metrics.iter().any(|e| e.is_gt()) { } else if self.ore > other.ore {
Ordering::Greater greater = true;
// 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()) { if self.clay < other.clay {
Ordering::Less less = true;
// In all other cases no definitive statement can be made. } else if self.clay > other.clay {
} else { 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 Ordering::Equal
} else if less {
Ordering::Less
} else {
Ordering::Greater
} }
} }
#[allow(dead_code)]
fn print(&self) { fn print(&self) {
print!("{:>3} O, ", self.ore); print!("{:>3} O, ", self.ore);
print!("{:>3} C, ", self.clay); print!("{:>3} C, ", self.clay);