aoc/year2017/day15.rs
1//! # Dueling Generators
2//!
3//! Multithreaded approach using worker threads to generate batches of numbers for judging.
4//! Part one can be checked in parallel, but part two must be sent to a single thread as the
5//! indices must be checked in order.
6//!
7//! The sequence of numbers is [modular exponentiation](https://en.wikipedia.org/wiki/Modular_exponentiation)
8//! so we can jump to any location in the sequence, without needing to know the previous numbers.
9//!
10//! The generator is in the hot path, so anything we can do to make it run faster is worthwhile.
11//! Start by observing that our divisor 0x7fffffff is of the form `2ᵏ - 1`, which lends itself
12//! well to computing a remainder with less work than a hardware division (the analysis here works
13//! for any number adjacent to a power of two, not just Mersenne primes). At a high level,
14//! computing `X % Y` is the same as repeatedly subtracting `Y` from a starting point of `X` until
15//! reaching a value less than `Y`. How many times does that subtraction occur? That's easy,
16//! `X / Y`. But when dividing by `Y` is expensive (a hardware division by an odd number takes
17//! multiple clock cycles), what if we divide by `Y + 1` instead (dividing by 2ᵏ is just
18//! performing a bit mask). Conceptually, the remainder after each subtraction of `Y + 1`
19//! grows by an error of one until we reach a remainder of `X % (Y + 1)` - but we know the total
20//! error, which was the number of times we subtracted the denominator, or `X / (Y + 1)`, and
21//! that value is also available, with just a bit shift. Adding the 31-bit adjusted remainder
22//! with the 31-bit error can overflow to 32 bits, so a final comparison against `MOD` gets
23//! the correct answer in `fast_mod` faster than hardware division. See also
24//! [this post](https://www.reddit.com/r/adventofcode/comments/7jxkiw/comment/drazokj/).
25use std::sync::mpsc::{Receiver, Sender, channel};
26use std::thread;
27
28use crate::util::hash::*;
29use crate::util::iter::*;
30use crate::util::math::*;
31use crate::util::parse::*;
32use crate::util::thread::*;
33
34const MOD: usize = 0x7fffffff;
35const PART_ONE: usize = 40_000_000;
36const PART_TWO: usize = 5_000_000;
37const BLOCK: usize = 50_000;
38
39type Input = (usize, usize);
40
41/// State shared between all threads.
42pub struct Shared {
43 first: usize,
44 second: usize,
45 iter: AtomicIter,
46}
47
48/// Generated numbers from `start` to `start + BLOCK`.
49struct Block {
50 start: usize,
51 ones: usize,
52 fours: Vec<u16>,
53 eights: Vec<u16>,
54}
55
56pub fn parse(input: &str) -> Input {
57 let [first, second] = input.iter_unsigned().chunk::<2>().next().unwrap();
58 let shared = Shared { first, second, iter: AtomicIter::new(0, BLOCK as u32) };
59 let (tx, rx) = channel();
60
61 thread::scope(|scope| {
62 // Use all cores except one to generate blocks of numbers for judging.
63 for _ in 0..threads() - 1 {
64 scope.spawn(|| sender(&shared, &tx));
65 }
66 // Judge batches serially.
67 receiver(&shared, &rx)
68 })
69}
70
71pub fn part1(input: &Input) -> usize {
72 input.0
73}
74
75pub fn part2(input: &Input) -> usize {
76 input.1
77}
78
79fn sender(shared: &Shared, tx: &Sender<Block>) {
80 while let Some(start) = shared.iter.next() {
81 // Start at any point in the sequence using modular exponentiation.
82 let start = start as usize;
83 let mut first = shared.first * 16807.mod_pow(start, MOD);
84 let mut second = shared.second * 48271.mod_pow(start, MOD);
85
86 // Estimate capacity at one quarter or one eighth.
87 let mut ones = 0;
88 let mut fours = Vec::with_capacity(BLOCK / 4);
89 let mut eights = Vec::with_capacity(BLOCK / 8);
90
91 // Check part one pairs immediately while queueing part two pairs.
92 for _ in 0..BLOCK {
93 first = fast_mod(first * 16807);
94 second = fast_mod(second * 48271);
95
96 let left = first as u16;
97 let right = second as u16;
98
99 if left == right {
100 ones += 1;
101 }
102 if left.is_multiple_of(4) {
103 fours.push(left);
104 }
105 if right.is_multiple_of(8) {
106 eights.push(right);
107 }
108 }
109
110 let _unused = tx.send(Block { start, ones, fours, eights });
111 }
112}
113
114fn receiver(shared: &Shared, rx: &Receiver<Block>) -> Input {
115 let mut required = 0;
116 let mut out_of_order = FastMap::new();
117
118 let mut fours = Vec::with_capacity(PART_TWO + BLOCK);
119 let mut eights = Vec::with_capacity(PART_TWO + BLOCK);
120 let mut start = 0;
121
122 let mut part_one = 0;
123 let mut part_two = 0;
124
125 while required < PART_ONE || fours.len() < PART_TWO || eights.len() < PART_TWO {
126 // Blocks could be received in any order, as there's no guarantee threads will finish
127 // processing at the same time. The `start` field of the block defines the order they
128 // must be added to the vec.
129 while let Ok(block) = rx.try_recv() {
130 out_of_order.insert(block.start, block);
131 }
132
133 while let Some(block) = out_of_order.remove(&required) {
134 required += BLOCK;
135
136 if required <= PART_ONE {
137 part_one += block.ones;
138 }
139
140 if fours.len() < PART_TWO {
141 fours.extend_from_slice(&block.fours);
142 }
143
144 if eights.len() < PART_TWO {
145 eights.extend_from_slice(&block.eights);
146 }
147
148 let end = PART_TWO.min(fours.len()).min(eights.len());
149 part_two +=
150 fours[start..end].iter().zip(&eights[start..end]).filter(|(a, b)| a == b).count();
151 start = end;
152 }
153 }
154
155 // Signal worker threads to finish.
156 shared.iter.stop();
157
158 (part_one, part_two)
159}
160
161/// Fast computation of n % 0x7fffffff.
162#[inline]
163fn fast_mod(n: usize) -> usize {
164 let low = n & MOD;
165 let high = n >> 31;
166 let sum = low + high;
167 if sum < MOD { sum } else { sum - MOD }
168}