aoc/year2018/day14.rs
1//! # Chocolate Charts
2//!
3//! This solution is heavily inspired by [Askalski's](https://www.reddit.com/user/askalski/)
4//! excellent post [Breaking the 1 billion recipes per second barrier](https://www.reddit.com/r/adventofcode/comments/a6wpwa/2018_day_14_breaking_the_1_billion_recipes_per/).
5//!
6//! The key insight is that after 23 recipes the elves converge into using the *same subset* of
7//! recipes. This subset can be stored compactly in about 20% of the space and read sequentially
8//! to allow efficient vector processing.
9//!
10//! Tricks used to speed things up:
11//! * Separate writer and reader threads to generate recipes and check them in parallel.
12//! * Vector processing of recipes using techniques similar to SIMD.
13use std::sync::atomic::{AtomicBool, Ordering};
14use std::sync::mpsc::{Receiver, Sender, channel};
15use std::thread;
16
17use crate::util::parse::*;
18
19/// Pre-calculate the first 23 recipes.
20const PREFIX: [u8; 23] = [3, 7, 1, 0, 1, 0, 1, 2, 4, 5, 1, 5, 8, 9, 1, 6, 7, 7, 9, 2, 5, 1, 0];
21
22type Input = (String, usize);
23
24pub fn parse(input: &str) -> Input {
25 // Send batches of recipes from the writer to the reader for checking.
26 let (tx, rx) = channel();
27 // Thread safe flag to let writer know when to stop.
28 let done = AtomicBool::new(false);
29 // Store recipes in fixed-size vec prefilled with ones. Part two result is around 20 million
30 // so size should be sufficient for most inputs.
31 let mut recipes = vec![1; 25_000_000];
32
33 thread::scope(|scope| {
34 // Start writer thread to produce new recipes.
35 scope.spawn(|| writer(tx, &done, recipes.as_mut_slice()));
36 // Reader thread checks recipes for the answers, returning when both parts are found.
37 scope.spawn(|| reader(rx, &done, input)).join().unwrap()
38 })
39}
40
41pub fn part1(input: &Input) -> &str {
42 &input.0
43}
44
45pub fn part2(input: &Input) -> usize {
46 input.1
47}
48
49/// Receives batches of recipes from the writer thread, then scans them byte by byte searching
50/// for the part two pattern. For simplicity the pattern is always assumed to be six digits.
51fn reader(rx: Receiver<&[u8]>, done: &AtomicBool, input: &str) -> (String, usize) {
52 let part_one_target = input.unsigned::<usize>() + 10;
53 let part_two_target = u32::from_str_radix(input.trim(), 16).unwrap();
54
55 let mut part_one_result = None;
56 let mut part_two_result = None;
57
58 let mut history = Vec::new();
59 let mut total = 0;
60 let mut pattern = 0;
61
62 for slice in rx {
63 history.push(slice);
64 total += slice.len();
65
66 // The recipes are broken up into batches. Even though these batches originally come
67 // from the same contiguous slice, this thread has no way to know that or reassemble
68 // the original. The result could potentially be split over two or more slices.
69 if part_one_result.is_none() && total >= part_one_target {
70 let mut index = 0;
71 let mut offset = part_one_target - 10;
72 let mut result = String::new();
73
74 for _ in 0..10 {
75 // If we go past the end of a slice then check the next one.
76 while offset >= history[index].len() {
77 offset -= history[index].len();
78 index += 1;
79 }
80
81 // Push each digit into a string as there could be leading zeroes.
82 let digit = history[index][offset];
83 result.push((digit + b'0') as char);
84 offset += 1;
85 }
86
87 part_one_result = Some(result);
88 }
89
90 // Simple brute force pattern matching. Slices are received in order so the pattern will
91 // handle cases when the target is split between two slices.
92 if part_two_result.is_none() {
93 for (i, n) in slice.iter().copied().enumerate() {
94 pattern = ((pattern << 4) | (n as u32)) & 0xffffff;
95
96 if pattern == part_two_target {
97 part_two_result = Some(total - slice.len() + i - 5);
98 break;
99 }
100 }
101 }
102
103 // Signal the writer thread to finish once both results are found.
104 if part_one_result.is_some() && part_two_result.is_some() {
105 done.store(true, Ordering::Relaxed);
106 break;
107 }
108 }
109
110 (part_one_result.unwrap(), part_two_result.unwrap())
111}
112
113/// Generates recipes then sends them to the reader thread for checking in batches.
114/// Processing is broken into alternating "cold" and "hot" loops. An outer enclosing loop checks
115/// periodically for the done signal from the reader thread.
116///
117/// The "cold" loop processes recipes serially one by one but can handle input corner cases.
118/// It's used when either:
119/// * One or both elves are within the first 23 recipes.
120/// * One or both elves are within the last 16 recipes.
121///
122/// The "hot" loop processes recipes efficiently in chunks of 16. The vast majority of recipes
123/// are calculated in this loop. As much as possible is parallelized using techniques similar to
124/// SIMD but using regular instructions instead of SIMD intrinsics or Rust's portable SIMD API.
125///
126/// Interestingly, on an Apple M2 Max this "poor man's SIMD" has the same performance as using
127/// the portable SIMD API. This is probably due to the fact that the serial loops that write new
128/// recipes take the majority of the time.
129fn writer<'a>(tx: Sender<&'a [u8]>, done: &AtomicBool, mut recipes: &'a mut [u8]) {
130 // The first 23 recipes have already been generated
131 // so the elves start at position 0 and 8 respectively.
132 let mut elf1 = 0;
133 let mut index1 = 0;
134
135 let mut elf2 = 8;
136 let mut index2 = 0;
137
138 let mut base = 0;
139 let mut size = 23;
140 let mut needed = 23;
141
142 // Store the smaller subset of recipes used by the elves.
143 let mut write = 0;
144 let mut snack: Vec<u8> = vec![0; 5_000_000];
145
146 while !done.load(Ordering::Relaxed) {
147 // Cold loop to handle start and end transitions.
148 while elf1 < 23 || elf2 < 23 || write - index1.max(index2) <= 16 {
149 // After the first 23 recipes both elves converge on the same set of ingredients.
150 let recipe1 = if elf1 < 23 {
151 PREFIX[elf1]
152 } else {
153 index1 += 1;
154 snack[index1 - 1]
155 };
156
157 let recipe2 = if elf2 < 23 {
158 PREFIX[elf2]
159 } else {
160 index2 += 1;
161 snack[index2 - 1]
162 };
163
164 // Add next recipe.
165 let next = recipe1 + recipe2;
166 if next < 10 {
167 recipes[size - base] = next;
168 size += 1;
169 } else {
170 recipes[size - base + 1] = next - 10;
171 size += 2;
172 }
173
174 if needed < size {
175 let digit = recipes[needed - base];
176 needed += 1 + digit as usize;
177
178 snack[write] = digit;
179 write += 1;
180 }
181
182 // Wrap around to start if necessary.
183 elf1 += 1 + recipe1 as usize;
184 if elf1 >= size {
185 elf1 -= size;
186 index1 = 0;
187 }
188
189 elf2 += 1 + recipe2 as usize;
190 if elf2 >= size {
191 elf2 -= size;
192 index2 = 0;
193 }
194 }
195
196 // Hot loop to handle the majority of recipes in the middle. Process at most 10,000 recipes
197 // at a time in order to produce batches between 160,000 and 320,000 bytes in size.
198 // This size is roughly tuned in order to maximize reader thread throughput.
199 let batch_size = 10_000.min((write - index1.max(index2) - 1) / 16);
200
201 for _ in 0..batch_size {
202 // Snacks can be processed sequentially.
203 let first = from_be_bytes(&snack, index1);
204 let second = from_be_bytes(&snack, index2);
205 let third = from_be_bytes(&snack, index1 + 8);
206 let fourth = from_be_bytes(&snack, index2 + 8);
207
208 // Each elf will skip forward between 16 and 32 snacks.
209 elf1 += 16 + lsb(prefix_sum(first)) + lsb(prefix_sum(third));
210 elf2 += 16 + lsb(prefix_sum(second)) + lsb(prefix_sum(fourth));
211 index1 += 16;
212 index2 += 16;
213
214 // Process the digits in parallel using techniques similar to SIMD.
215 let (digits1, indices1, extra1) = unpack(first, second);
216 let (digits2, indices2, extra2) = unpack(third, fourth);
217
218 // Scatter each digit into the correct location, leaving "holes" where ones should go.
219 // This is handled correctly by prefilling `recipes` with ones when initializing.
220 for shift in (0..64).step_by(8) {
221 let digit = lsb(digits1 >> shift);
222 let index = lsb(indices1 >> shift);
223 recipes[size - base + index] = digit as u8;
224
225 let digit = lsb(digits2 >> shift);
226 let index = lsb(indices2 >> shift);
227 recipes[size - base + index + extra1] = digit as u8;
228 }
229
230 size += extra1 + extra2;
231
232 // Write the recipes that will actually be used in subsequent loops to a smaller
233 // contiguous vec.
234 while needed < size {
235 let digit = recipes[needed - base];
236 needed += 1 + digit as usize;
237
238 snack[write] = digit;
239 write += 1;
240 }
241 }
242
243 // Split the mutable `recipes` slice into two parts. This allows the reader thread to
244 // access the head in parallel while the writer thread continues to write to the tail,
245 // ensuring unique ownership of each part of memory to prevent any concurrency issues.
246 let (head, tail) = recipes.split_at_mut(size - base);
247 let _unused = tx.send(head);
248 recipes = tail;
249 base = size;
250 }
251
252 // Drop the sender to make the receiver hang up.
253 drop(tx);
254}
255
256/// Convert 8 bytes in [big endian order](https://en.wikipedia.org/wiki/Endianness) into a `usize`.
257#[inline]
258fn from_be_bytes(slice: &[u8], index: usize) -> usize {
259 usize::from_be_bytes(slice[index..index + 8].try_into().unwrap())
260}
261
262/// Convenience function that returns least significant byte.
263#[inline]
264fn lsb(u: usize) -> usize {
265 u & 0xff
266}
267
268/// Compute the prefix sum of each byte within a `usize`. Let `a..h` denote the bytes from most
269/// significant to least significant and `Σx..y` denote the sum from `x` to `y` inclusive.
270///
271/// ```none
272/// s | a | b | c | d | e | f | g | h |
273/// s += (s >> 8) | a | Σa..b | Σb..c | Σc..d | Σd..e | Σe..f | Σf..g | Σg..h |
274/// s += (s >> 16) | a | Σa..b | Σa..c | Σa..d | Σb..e | Σc..f | Σd..g | Σe..h |
275/// s += (s >> 32) | a | Σa..b | Σa..c | Σa..d | Σa..e | Σa..f | Σa..g | Σa..h |
276/// ```
277#[inline]
278fn prefix_sum(u: usize) -> usize {
279 let mut s = u;
280 s += s >> 8;
281 s += s >> 16;
282 s += s >> 32;
283 s
284}
285
286/// Takes two groups of 8 digits each packed into a `usize` as input, then returns the output
287/// digits and their respective locations. Ones from sums greater than ten are implicit and not
288/// included since recipes has already been pre-filled with ones.
289#[inline]
290fn unpack(first: usize, second: usize) -> (usize, usize, usize) {
291 const ONES: usize = 0x0101010101010101;
292 const SIXES: usize = 0x0606060606060606;
293 const INDICES: usize = 0x0001020304050607;
294
295 // Example values, showing each byte in a column:
296 //
297 // first | 04 | 01 | 09 | 08 | 00 | 03 | 05 | 07 |
298 // second | 03 | 00 | 02 | 04 | 09 | 06 | 05 | 01 |
299 // sum | 07 | 01 | 0b | 0c | 09 | 09 | 0a | 08 |
300 let sum = first + second;
301
302 // Add 6 to each byte so that sums greater than or equal to ten become greater than or equal
303 // to 16, setting the first bit in the high nibble of each byte.
304 //
305 // sum | 07 | 01 | 0b | 0c | 09 | 09 | 0a | 08 |
306 // SIXES | 06 | 06 | 06 | 06 | 06 | 06 | 06 | 06 |
307 // total | 0d | 07 | 11 | 12 | 0f | 0f | 10 | 0e |
308 // tens | 00 | 00 | 01 | 01 | 00 | 00 | 01 | 00 |
309 let tens = ((sum + SIXES) >> 4) & ONES;
310
311 // Multiply by 10 to "spread" a 10 into each byte that has a total greater than 10.
312 //
313 // tens | 00 | 00 | 01 | 01 | 00 | 00 | 01 | 00 |
314 // tens * 10 | 00 | 00 | 0a | 0a | 00 | 00 | 0a | 00 |
315 // digits | 07 | 01 | 01 | 02 | 09 | 09 | 00 | 08 |
316 let digits = sum - 10 * tens;
317
318 // Columns greater than 10 will take 2 bytes when written to recipes. Each index is
319 // offset by the number of 10s before it. Adding the normal increase indices gives the
320 // final location of each byte.
321 //
322 // tens | 00 | 00 | 01 | 01 | 00 | 00 | 01 | 00 |
323 // prefix sum | 00 | 00 | 01 | 02 | 02 | 02 | 03 | 03 |
324 // INDICES | 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 |
325 // indices | 00 | 02 | 03 | 05 | 06 | 07 | 09 | 0a |
326 let indices = prefix_sum(tens) + INDICES;
327
328 // The total number of bytes that need to be written is one plus the last index.
329 let extra = 1 + lsb(indices);
330
331 (digits, indices, extra)
332}