44//! Keeping each thread busy and spreading the work as evenly as possible is quite tricky. Some
55//! paths can dead-end quickly while others can take the majority of exploration time.
66//!
7- //! To solve this we implement a very simple version of
8- //! [work stealing](https://en.wikipedia.org/wiki/Work_stealing). Threads process paths locally,
7+ //! To solve this we implement a very simple version of work sharing. Threads process paths locally
98//! stopping every now and then to return paths to a global queue. This allows other threads that
109//! have run out of work to pickup new paths to process.
1110//!
@@ -16,23 +15,19 @@ use crate::util::md5::*;
1615use crate :: util:: thread:: * ;
1716use std:: sync:: { Condvar , Mutex } ;
1817
19- type Input = ( String , usize ) ;
18+ type Input = ( Vec < u8 > , usize ) ;
2019type Item = ( u8 , u8 , usize , Vec < u8 > ) ;
2120
2221struct State {
2322 todo : Vec < Item > ,
24- min : String ,
23+ min : Vec < u8 > ,
2524 max : usize ,
26- }
27-
28- struct Exclusive {
29- global : State ,
3025 inflight : usize ,
3126}
3227
3328struct Shared {
3429 prefix : usize ,
35- mutex : Mutex < Exclusive > ,
30+ mutex : Mutex < State > ,
3631 not_empty : Condvar ,
3732}
3833
@@ -43,19 +38,18 @@ pub fn parse(input: &str) -> Input {
4338 let start = ( 0 , 0 , prefix, extend ( input, prefix, 0 ) ) ;
4439
4540 // State shared between threads.
46- let global = State { todo : vec ! [ start] , min : String :: new ( ) , max : 0 } ;
47- let exclusive = Exclusive { global, inflight : 0 } ;
48- let shared = Shared { prefix, mutex : Mutex :: new ( exclusive) , not_empty : Condvar :: new ( ) } ;
41+ let state = State { todo : vec ! [ start] , min : vec ! [ ] , max : 0 , inflight : threads ( ) } ;
42+ let shared = Shared { prefix, mutex : Mutex :: new ( state) , not_empty : Condvar :: new ( ) } ;
4943
5044 // Search paths in parallel.
5145 spawn ( || worker ( & shared) ) ;
5246
53- let global = shared. mutex . into_inner ( ) . unwrap ( ) . global ;
54- ( global . min , global . max )
47+ let state = shared. mutex . into_inner ( ) . unwrap ( ) ;
48+ ( state . min , state . max )
5549}
5650
5751pub fn part1 ( input : & Input ) -> & str {
58- & input. 0
52+ str :: from_utf8 ( & input. 0 ) . unwrap ( )
5953}
6054
6155pub fn part2 ( input : & Input ) -> usize {
@@ -65,45 +59,47 @@ pub fn part2(input: &Input) -> usize {
6559/// Process local work items, stopping every now and then to redistribute items back to global pool.
6660/// This prevents threads idling or hotspotting.
6761fn worker ( shared : & Shared ) {
68- let mut local = State { todo : Vec :: new ( ) , min : String :: new ( ) , max : 0 } ;
62+ let mut local = State { todo : vec ! [ ] , min : vec ! [ ] , max : 0 , inflight : 0 } ;
6963
7064 loop {
71- let mut exclusive = shared. mutex . lock ( ) . unwrap ( ) ;
72- let item = loop {
73- // Pickup available work.
74- if let Some ( item) = exclusive. global . todo . pop ( ) {
75- exclusive. inflight += 1 ;
76- break item;
77- }
78- // If no work available and no other thread is doing anything, then we're done.
79- if exclusive. inflight == 0 {
80- return ;
81- }
82- // Put thread to sleep until another thread notifies us that work is available.
83- // This avoids busy looping on the mutex.
84- exclusive = shared. not_empty . wait ( exclusive) . unwrap ( ) ;
85- } ;
86-
87- // Drop mutex to release lock and allow other threads access.
88- drop ( exclusive) ;
89-
9065 // Process local work items.
91- local. todo . push ( item) ;
9266 explore ( shared, & mut local) ;
9367
94- // Redistribute local work items back to the global queue. Update min and max paths.
95- let mut exclusive = shared. mutex . lock ( ) . unwrap ( ) ;
96- let global = & mut exclusive. global ;
68+ // Acquire mutex.
69+ let mut state = shared. mutex . lock ( ) . unwrap ( ) ;
9770
98- global. todo . append ( & mut local. todo ) ;
99- if global. min . is_empty ( ) || local. min . len ( ) < global. min . len ( ) {
100- global. min = local. min . clone ( ) ;
71+ // Update min and max paths.
72+ if state. min . is_empty ( ) || local. min . len ( ) < state. min . len ( ) {
73+ state. min . clone_from ( & local. min ) ;
74+ }
75+ state. max = state. max . max ( local. max ) ;
76+
77+ if local. todo . is_empty ( ) {
78+ // Mark ourselves as idle then notify all other threads in case we're done.
79+ state. inflight -= 1 ;
80+ shared. not_empty . notify_all ( ) ;
81+
82+ loop {
83+ // Pickup available work.
84+ if let Some ( item) = state. todo . pop ( ) {
85+ state. inflight += 1 ;
86+ local. todo . push ( item) ;
87+ break ;
88+ }
89+ // If no work available and no other thread is doing anything, then we're done.
90+ if state. inflight == 0 {
91+ return ;
92+ }
93+ // Put thread to sleep until another thread notifies us that work is available.
94+ // This avoids busy looping on the mutex.
95+ state = shared. not_empty . wait ( state) . unwrap ( ) ;
96+ }
97+ } else {
98+ // Redistribute excess local work items back to the global queue then notify all other
99+ // threads that there is new work available.
100+ state. todo . extend ( local. todo . drain ( 1 ..) ) ;
101+ shared. not_empty . notify_all ( ) ;
101102 }
102- global. max = global. max . max ( local. max ) ;
103-
104- // Mark ourselves as idle then notify all other threads that there is new work available.
105- exclusive. inflight -= 1 ;
106- shared. not_empty . notify_all ( ) ;
107103 }
108104}
109105
@@ -121,37 +117,41 @@ fn explore(shared: &Shared, local: &mut State) {
121117 let adjusted = size - shared. prefix ;
122118 if local. min . is_empty ( ) || adjusted < local. min . len ( ) {
123119 // Remove salt and padding.
124- let middle = path[ shared. prefix ..size] . to_vec ( ) ;
125- local. min = String :: from_utf8 ( middle) . unwrap ( ) ;
120+ local. min = path[ shared. prefix ..size] . to_vec ( ) ;
126121 }
127122 local. max = local. max . max ( adjusted) ;
128123 } else {
129124 // Explore other paths.
130125 let [ result, ..] = hash ( & mut path, size) ;
131126
132- if y > 0 && ( ( result >> 28 ) & 0xf ) > 0xa {
127+ if y > 0 && is_open ( result, 28 ) {
133128 local. todo . push ( ( x, y - 1 , size + 1 , extend ( & path, size, b'U' ) ) ) ;
134129 }
135- if y < 3 && ( ( result >> 24 ) & 0xf ) > 0xa {
130+ if y < 3 && is_open ( result, 24 ) {
136131 local. todo . push ( ( x, y + 1 , size + 1 , extend ( & path, size, b'D' ) ) ) ;
137132 }
138- if x > 0 && ( ( result >> 20 ) & 0xf ) > 0xa {
133+ if x > 0 && is_open ( result, 20 ) {
139134 local. todo . push ( ( x - 1 , y, size + 1 , extend ( & path, size, b'L' ) ) ) ;
140135 }
141- if x < 3 && ( ( result >> 16 ) & 0xf ) > 0xa {
136+ if x < 3 && is_open ( result, 16 ) {
142137 local. todo . push ( ( x + 1 , y, size + 1 , extend ( & path, size, b'R' ) ) ) ;
143138 }
144139 }
145140 }
146141}
147142
143+ /// Check if a door is open based on MD5 hex digit (b-f means open).
144+ #[ inline]
145+ fn is_open ( hash : u32 , shift : u32 ) -> bool {
146+ ( ( hash >> shift) & 0xf ) > 0xa
147+ }
148+
148149/// Convenience function to generate new path.
149150fn extend ( src : & [ u8 ] , size : usize , b : u8 ) -> Vec < u8 > {
150151 // Leave room for MD5 padding.
151- let padded = buffer_size ( size + 1 ) ;
152- let mut next = vec ! [ 0 ; padded] ;
152+ let mut next = vec ! [ 0 ; buffer_size( size + 1 ) ] ;
153153 // Copy existing path and next step.
154- next[ 0 ..size] . copy_from_slice ( & src[ 0 ..size] ) ;
154+ next[ ..size] . copy_from_slice ( & src[ ..size] ) ;
155155 next[ size] = b;
156156 next
157157}
0 commit comments