Skip to main content

aoc/year2016/
day14.rs

1//! # One-Time Pad
2//!
3//! Brute force slog through all possible keys, parallelized as much as possible. An optimization
4//! for part two is a quick method to convert `u32` to 8 ASCII digits.
5use core::fmt::NumBuffer;
6use std::collections::{BTreeMap, BTreeSet};
7use std::sync::Mutex;
8
9use self::implementation::*;
10use crate::util::md5::*;
11use crate::util::thread::*;
12
13/// Atomics can be safely shared between threads.
14struct Shared {
15    input: String,
16    part_two: bool,
17    iter: AtomicIter,
18    mutex: Mutex<Exclusive>,
19}
20
21/// Regular data structures need to be protected by a mutex.
22struct Exclusive {
23    threes: BTreeMap<i32, u32>,
24    fives: BTreeMap<i32, u32>,
25    found: BTreeSet<i32>,
26}
27
28pub fn parse(input: &str) -> &str {
29    input.trim()
30}
31
32/// Hash each key once.
33pub fn part1(input: &str) -> i32 {
34    generate_pad(input, false)
35}
36
37/// Hash each key an additional 2016 times.
38pub fn part2(input: &str) -> i32 {
39    generate_pad(input, true)
40}
41
42/// Find the first 64 keys that satisfy the rules.
43fn generate_pad(input: &str, part_two: bool) -> i32 {
44    let step = if cfg!(feature = "simd") { 32 } else { 1 };
45    let input = input.to_owned();
46
47    let exclusive =
48        Exclusive { threes: BTreeMap::new(), fives: BTreeMap::new(), found: BTreeSet::new() };
49    let shared =
50        Shared { input, part_two, iter: AtomicIter::new(0, step), mutex: Mutex::new(exclusive) };
51
52    // Use as many cores as possible to parallelize the search.
53    spawn(|| worker(&shared));
54
55    let exclusive = shared.mutex.into_inner().unwrap();
56    *exclusive.found.iter().nth(63).unwrap()
57}
58
59/// Write the salt and integer index as ASCII characters.
60fn format_string(prefix: &str, n: i32) -> ([u8; 64], usize) {
61    let mut number = NumBuffer::new();
62    let digits = n.format_into(&mut number).as_bytes();
63    let size = prefix.len() + digits.len();
64
65    let mut buffer = [0; 64];
66    buffer[..prefix.len()].copy_from_slice(prefix.as_bytes());
67    buffer[prefix.len()..size].copy_from_slice(digits);
68
69    (buffer, size)
70}
71
72/// Quickly convert a `u32` to an array of 8 ASCII values.
73#[inline]
74fn to_ascii(n: u32) -> [u8; 8] {
75    // Spread each nibble into its own byte, for example `1234abcd` becomes `010203040a0b0c0d`.
76    let mut n = u64::from(n);
77    n = ((n << 16) & 0x0000ffff00000000) | (n & 0x000000000000ffff);
78    n = ((n << 8) & 0x00ff000000ff0000) | (n & 0x000000ff000000ff);
79    n = ((n << 4) & 0x0f000f000f000f00) | (n & 0x000f000f000f000f);
80
81    // If a digit is 0 to 9 then we need to add `0x30` to convert to an ASCII digit.
82    // For digits from 10 to 15 we need to further add `0x27` to convert to lowercase ASCII.
83    // Steps:
84    // * Add 6 to each digit
85    // * If digit is 10 or higher then the highest bit in each nibble will be set
86    // * Shift this bit to create a mask
87    // * Multiply mask by 0x27 to get ASCII conversion offset
88    // For example, mask of `010203040a0b0c0d` is `0000000001010101`.
89
90    let mask = ((n + 0x0606060606060606) >> 4) & 0x0101010101010101;
91    n = n + 0x3030303030303030 + 0x27 * mask;
92    n.to_be_bytes()
93}
94
95#[cfg(not(feature = "simd"))]
96mod implementation {
97    use super::*;
98
99    pub(super) fn worker(shared: &Shared) {
100        while let Some(n) = shared.iter.next() {
101            // Get the next key to check.
102            let n = n as i32;
103
104            // Calculate the hash.
105            let (mut buffer, size) = format_string(&shared.input, n);
106            let mut result = hash(&mut buffer, size);
107
108            if shared.part_two {
109                for _ in 0..2016 {
110                    buffer[0..8].copy_from_slice(&to_ascii(result[0]));
111                    buffer[8..16].copy_from_slice(&to_ascii(result[1]));
112                    buffer[16..24].copy_from_slice(&to_ascii(result[2]));
113                    buffer[24..32].copy_from_slice(&to_ascii(result[3]));
114                    result = hash(&mut buffer, 32);
115                }
116            }
117
118            check(shared, n, result);
119        }
120    }
121
122    /// Check for sequences of 3 or 5 consecutive matching digits.
123    fn check(shared: &Shared, n: i32, hash: [u32; 4]) {
124        let [a, b, c, d] = hash;
125
126        let mut prev = u32::MAX;
127        let mut same = 1;
128        let mut three = 0;
129        let mut five = 0;
130
131        for mut word in [d, c, b, a] {
132            for _ in 0..8 {
133                let next = word & 0xf;
134
135                same = if next == prev { same + 1 } else { 1 };
136
137                if same == 3 {
138                    three = 1 << next;
139                }
140                if same == 5 {
141                    five |= 1 << next;
142                }
143
144                word >>= 4;
145                prev = next;
146            }
147        }
148
149        if three != 0 || five != 0 {
150            let mut exclusive = shared.mutex.lock().unwrap();
151            let mut candidates = Vec::new();
152
153            // Compare against all 5 digit sequences.
154            if three != 0 {
155                exclusive.threes.insert(n, three);
156
157                for (_, mask) in exclusive.fives.range(n + 1..n + 1001) {
158                    if three & mask != 0 {
159                        candidates.push(n);
160                    }
161                }
162            }
163
164            // Compare against all 3 digit sequences.
165            if five != 0 {
166                exclusive.fives.insert(n, five);
167
168                for (&index, &mask) in exclusive.threes.range(n - 1000..n) {
169                    if five & mask != 0 {
170                        candidates.push(index);
171                    }
172                }
173            }
174
175            // Add any matching keys found, finishing once we have at least 64 keys.
176            exclusive.found.extend(candidates);
177
178            if exclusive.found.len() >= 64 {
179                shared.iter.stop();
180            }
181        }
182    }
183}
184
185#[cfg(feature = "simd")]
186mod implementation {
187    use std::simd::prelude::*;
188
189    use super::*;
190    use crate::util::bitset::*;
191    use crate::util::md5::simd::hash_fixed;
192
193    /// Use SIMD to compute hashes in parallel in blocks of 32.
194    pub(super) fn worker(shared: &Shared) {
195        let mut result = [Simd::splat(0); 4];
196        let mut buffers = [[0; 64]; 32];
197
198        while let Some(start) = shared.iter.next() {
199            // Get the next key to check.
200            let start = start as i32;
201
202            // Calculate the hash.
203            for i in 0..32 {
204                let (mut buffer, size) = format_string(&shared.input, start + i as i32);
205                let [a, b, c, d] = hash(&mut buffer, size);
206
207                result[0][i] = a;
208                result[1][i] = b;
209                result[2][i] = c;
210                result[3][i] = d;
211            }
212
213            if shared.part_two {
214                for _ in 0..2016 {
215                    for i in 0..32 {
216                        buffers[i][0..8].copy_from_slice(&to_ascii(result[0][i]));
217                        buffers[i][8..16].copy_from_slice(&to_ascii(result[1][i]));
218                        buffers[i][16..24].copy_from_slice(&to_ascii(result[2][i]));
219                        buffers[i][24..32].copy_from_slice(&to_ascii(result[3][i]));
220                    }
221                    result = hash_fixed(&mut buffers, 32);
222                }
223            }
224
225            check(shared, start, &result);
226        }
227    }
228
229    /// Check for sequences of 3 or 5 consecutive matching digits.
230    #[inline]
231    fn check(shared: &Shared, start: i32, hash: &[Simd<u32, 32>; 4]) {
232        let &[a, b, c, d] = hash;
233
234        let mut prev: Simd<u32, 32> = Simd::splat(u32::MAX);
235        let mut same: Simd<u32, 32> = Simd::splat(1);
236        let mut three: Simd<u32, 32> = Simd::splat(0);
237        let mut five: Simd<u32, 32> = Simd::splat(0);
238
239        for mut word in [d, c, b, a] {
240            for _ in 0..8 {
241                let next = word & Simd::splat(0xf);
242                same = next.simd_eq(prev).select(same + Simd::splat(1), Simd::splat(1));
243
244                three = same.simd_eq(Simd::splat(3)).select(Simd::splat(1) << next, three);
245                five |= same.simd_eq(Simd::splat(5)).select(Simd::splat(1) << next, Simd::splat(0));
246
247                word >>= 4;
248                prev = next;
249            }
250        }
251
252        let three_mask = three.simd_ne(Simd::splat(0)).to_bitmask();
253        let five_mask = five.simd_ne(Simd::splat(0)).to_bitmask();
254
255        if three_mask != 0 || five_mask != 0 {
256            let mut exclusive = shared.mutex.lock().unwrap();
257            let mut candidates = Vec::new();
258
259            for i in three_mask.biterator() {
260                let three = three[i];
261                let n = start + i as i32;
262
263                // Compare against all 5 digit sequences.
264                if three != 0 {
265                    exclusive.threes.insert(n, three);
266
267                    for (_, mask) in exclusive.fives.range(n + 1..n + 1001) {
268                        if three & mask != 0 {
269                            candidates.push(n);
270                        }
271                    }
272                }
273            }
274
275            for i in five_mask.biterator() {
276                let five = five[i];
277                let n = start + i as i32;
278
279                // Compare against all 3 digit sequences.
280                if five != 0 {
281                    exclusive.fives.insert(n, five);
282
283                    for (&index, &mask) in exclusive.threes.range(n - 1000..n) {
284                        if five & mask != 0 {
285                            candidates.push(index);
286                        }
287                    }
288                }
289            }
290
291            // Add any matching keys found, finishing once we have at least 64 keys.
292            exclusive.found.extend(candidates);
293
294            if exclusive.found.len() >= 64 {
295                shared.iter.stop();
296            }
297        }
298    }
299}