aoc/year2015/
day04.rs

1//! # The Ideal Stocking Stuffer
2//!
3//! This solution relies on brute forcing combinations as quickly as possible using an internal
4//! implementation of the [`MD5`] hashing algorithm.
5//!
6//! Each number's hash is independent of the others, so we speed things up by using threading
7//! to search in parallel in blocks of 1000 numbers at a time.
8//!
9//! Using the [`format!`] macro to join the secret key to the number is quite slow. To go faster
10//! we reuse the same `u8` buffer, incrementing digits one at a time.
11//! The numbers from 1 to 999 are handled specially.
12//!
13//! Interestingly the total time to solve this problem is *extremely* sensitive to the secret key
14//! provided as input. For example my key required ~10⁷ iterations to find the answer to part two.
15//! However for unit testing, I was able to randomly find a value that takes only 455 iterations,
16//! about 22,000 times faster!
17//!
18//! [`MD5`]: crate::util::md5
19//! [`format!`]: std::format
20use crate::util::md5::*;
21use crate::util::thread::*;
22use std::sync::atomic::{AtomicU32, Ordering};
23
24pub struct Shared {
25    prefix: String,
26    iter: AtomicIter,
27    first: AtomicU32,
28    second: AtomicU32,
29}
30
31pub fn parse(input: &str) -> Shared {
32    let shared = Shared {
33        prefix: input.trim().to_owned(),
34        iter: AtomicIter::new(1000, 1000),
35        first: AtomicU32::new(u32::MAX),
36        second: AtomicU32::new(u32::MAX),
37    };
38
39    // Handle the first 999 numbers specially as the number of digits varies.
40    for n in 1..1000 {
41        let (mut buffer, size) = format_string(&shared.prefix, n);
42        check_hash(&mut buffer, size, n, &shared);
43    }
44
45    // Use as many cores as possible to parallelize the remaining search.
46    spawn(|| {
47        #[cfg(not(feature = "simd"))]
48        worker(&shared);
49        #[cfg(feature = "simd")]
50        simd::worker(&shared);
51    });
52
53    shared
54}
55
56pub fn part1(input: &Shared) -> u32 {
57    input.first.load(Ordering::Relaxed)
58}
59
60pub fn part2(input: &Shared) -> u32 {
61    input.second.load(Ordering::Relaxed)
62}
63
64fn format_string(prefix: &str, n: u32) -> ([u8; 64], usize) {
65    let string = format!("{prefix}{n}");
66    let size = string.len();
67
68    let mut buffer = [0; 64];
69    buffer[0..size].copy_from_slice(string.as_bytes());
70
71    (buffer, size)
72}
73
74fn check_hash(buffer: &mut [u8], size: usize, n: u32, shared: &Shared) {
75    let [result, ..] = hash(buffer, size);
76
77    if result & 0xffffff00 == 0 {
78        shared.second.fetch_min(n, Ordering::Relaxed);
79        shared.iter.stop();
80    } else if result & 0xfffff000 == 0 {
81        shared.first.fetch_min(n, Ordering::Relaxed);
82    }
83}
84
85#[cfg(not(feature = "simd"))]
86fn worker(shared: &Shared) {
87    while let Some(offset) = shared.iter.next() {
88        let (mut buffer, size) = format_string(&shared.prefix, offset);
89
90        for n in 0..1000 {
91            // Format macro is very slow, so update digits directly
92            buffer[size - 3] = b'0' + (n / 100) as u8;
93            buffer[size - 2] = b'0' + ((n / 10) % 10) as u8;
94            buffer[size - 1] = b'0' + (n % 10) as u8;
95
96            check_hash(&mut buffer, size, offset + n, shared);
97        }
98    }
99}
100
101#[cfg(feature = "simd")]
102mod simd {
103    use super::*;
104    use crate::util::bitset::*;
105    use crate::util::md5::simd::hash_fixed;
106    use std::simd::cmp::SimdPartialEq as _;
107    use std::simd::*;
108
109    #[expect(clippy::needless_range_loop)]
110    fn check_hash_simd<const N: usize>(
111        buffers: &mut [[u8; 64]; N],
112        size: usize,
113        start: u32,
114        offset: u32,
115        shared: &Shared,
116    ) where
117        LaneCount<N>: SupportedLaneCount,
118    {
119        // Format macro is very slow, so update digits directly
120        for i in 0..N {
121            let n = offset + i as u32;
122            buffers[i][size - 3] = b'0' + (n / 100) as u8;
123            buffers[i][size - 2] = b'0' + ((n / 10) % 10) as u8;
124            buffers[i][size - 1] = b'0' + (n % 10) as u8;
125        }
126
127        let [result, ..] = hash_fixed(buffers, size);
128        let bitmask = (result & Simd::splat(0xfffff000)).simd_eq(Simd::splat(0)).to_bitmask();
129
130        for i in bitmask.biterator() {
131            if result[i] & 0xffffff00 == 0 {
132                shared.second.fetch_min(start + offset + i as u32, Ordering::Relaxed);
133                shared.iter.stop();
134            } else {
135                shared.first.fetch_min(start + offset + i as u32, Ordering::Relaxed);
136            }
137        }
138    }
139
140    pub(super) fn worker(shared: &Shared) {
141        while let Some(start) = shared.iter.next() {
142            let (prefix, size) = format_string(&shared.prefix, start);
143            let buffers = &mut [prefix; 32];
144
145            for offset in (0..992).step_by(32) {
146                check_hash_simd(buffers, size, start, offset, shared);
147            }
148
149            let buffers = &mut [prefix; 8];
150            check_hash_simd(buffers, size, start, 992, shared);
151        }
152    }
153}