Skip to main content

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}