-
Notifications
You must be signed in to change notification settings - Fork 5
/
day17.rs
157 lines (136 loc) · 5.54 KB
/
day17.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
//! # Two Steps Forward
//!
//! Brute force search over every possible path. Work is parallelized over multiple threads.
//! Keeping each thread busy and spreading the work as evenly as possible is quite tricky. Some
//! paths can dead-end quickly while others can take the majority of exploration time.
//!
//! To solve this we implement a very simple version of
//! [work stealing](https://en.wikipedia.org/wiki/Work_stealing). Threads process paths locally,
//! stopping every now and then to return paths to a global queue. This allows other threads that
//! have run out of work to pickup new paths to process.
//!
//! The approach from "Waiting: Parking and Condition Variables" in the excellent book
//! [Rust Atomics and Locks](https://marabos.nl/atomics/) prevent idle threads from busy
//! looping on the mutex.
use crate::util::md5::*;
use crate::util::thread::*;
use std::sync::{Condvar, Mutex};
type Input = (String, usize);
type Item = (u8, u8, usize, Vec<u8>);
struct State {
todo: Vec<Item>,
min: String,
max: usize,
}
struct Exclusive {
global: State,
inflight: usize,
}
struct Shared {
prefix: usize,
mutex: Mutex<Exclusive>,
not_empty: Condvar,
}
pub fn parse(input: &str) -> Input {
// Initial starting position is the top left corner.
let input = input.trim().as_bytes();
let prefix = input.len();
let start = (0, 0, prefix, extend(input, prefix, 0));
// State shared between threads.
let global = State { todo: vec![start], min: String::new(), max: 0 };
let exclusive = Exclusive { global, inflight: 0 };
let shared = Shared { prefix, mutex: Mutex::new(exclusive), not_empty: Condvar::new() };
// Search paths in parallel.
spawn(|| worker(&shared));
let global = shared.mutex.into_inner().unwrap().global;
(global.min, global.max)
}
pub fn part1(input: &Input) -> &str {
&input.0
}
pub fn part2(input: &Input) -> usize {
input.1
}
/// Process local work items, stopping every now and then to redistribute items back to global pool.
/// This prevents threads idling or hotspotting.
fn worker(shared: &Shared) {
let mut local = State { todo: Vec::new(), min: String::new(), max: 0 };
loop {
let mut exclusive = shared.mutex.lock().unwrap();
let item = loop {
// Pickup available work.
if let Some(item) = exclusive.global.todo.pop() {
exclusive.inflight += 1;
break item;
}
// If no work available and no other thread is doing anything, then we're done.
if exclusive.inflight == 0 {
return;
}
// Put thread to sleep until another thread notifies us that work is available.
// This avoids busy looping on the mutex.
exclusive = shared.not_empty.wait(exclusive).unwrap();
};
// Drop mutex to release lock and allow other threads access.
drop(exclusive);
// Process local work items.
local.todo.push(item);
explore(shared, &mut local);
// Redistribute local work items back to the global queue. Update min and max paths.
let mut exclusive = shared.mutex.lock().unwrap();
let global = &mut exclusive.global;
global.todo.append(&mut local.todo);
if global.min.is_empty() || local.min.len() < global.min.len() {
global.min = local.min.clone();
}
global.max = global.max.max(local.max);
// Mark ourselves as idle then notify all other threads that there is new work available.
exclusive.inflight -= 1;
shared.not_empty.notify_all();
}
}
/// Explore at most 100 paths, stopping sooner if we run out.
/// 100 is chosen empirically as the amount that results in the least total time taken.
///
/// Too low and threads waste time locking the mutex, reading and writing global state.
/// Too high and some threads are starved with no paths, while other threads do all the work.
fn explore(shared: &Shared, local: &mut State) {
for _ in 0..100 {
let Some((x, y, size, mut path)) = local.todo.pop() else { break };
if x == 3 && y == 3 {
// Stop if we've reached the bottom right room.
let adjusted = size - shared.prefix;
if local.min.is_empty() || adjusted < local.min.len() {
// Remove salt and padding.
let middle = path[shared.prefix..size].to_vec();
local.min = String::from_utf8(middle).unwrap();
}
local.max = local.max.max(adjusted);
} else {
// Explore other paths.
let (result, ..) = hash(&mut path, size);
if y > 0 && ((result >> 28) & 0xf) > 0xa {
local.todo.push((x, y - 1, size + 1, extend(&path, size, b'U')));
}
if y < 3 && ((result >> 24) & 0xf) > 0xa {
local.todo.push((x, y + 1, size + 1, extend(&path, size, b'D')));
}
if x > 0 && ((result >> 20) & 0xf) > 0xa {
local.todo.push((x - 1, y, size + 1, extend(&path, size, b'L')));
}
if x < 3 && ((result >> 16) & 0xf) > 0xa {
local.todo.push((x + 1, y, size + 1, extend(&path, size, b'R')));
}
}
}
}
/// Convenience function to generate new path.
fn extend(src: &[u8], size: usize, b: u8) -> Vec<u8> {
// Leave room for MD5 padding.
let padded = buffer_size(size + 1);
let mut next = vec![0; padded];
// Copy existing path and next step.
next[0..size].copy_from_slice(&src[0..size]);
next[size] = b;
next
}