Skip to content

Commit 9ea4690

Browse files
committed
Parallelize search
1 parent 3d13637 commit 9ea4690

File tree

2 files changed

+91
-39
lines changed

2 files changed

+91
-39
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ pie
192192
| 1 | [Not Quite Lisp](https://adventofcode.com/2015/day/1) | [Source](src/year2015/day01.rs) | 2 |
193193
| 2 | [I Was Told There Would Be No Math](https://adventofcode.com/2015/day/2) | [Source](src/year2015/day02.rs) | 8 |
194194
| 3 | [Perfectly Spherical Houses in a Vacuum](https://adventofcode.com/2015/day/3) | [Source](src/year2015/day03.rs) | 100 |
195-
| 4 | [The Ideal Stocking Stuffer](https://adventofcode.com/2015/day/4) | [Source](src/year2015/day04.rs) | 858000 |
195+
| 4 | [The Ideal Stocking Stuffer](https://adventofcode.com/2015/day/4) | [Source](src/year2015/day04.rs) | 82000 |
196196
| 5 | [Doesn't He Have Intern-Elves For This?](https://adventofcode.com/2015/day/5) | [Source](src/year2015/day05.rs) | 39 |
197197
| 6 | [Probably a Fire Hazard](https://adventofcode.com/2015/day/6) | [Source](src/year2015/day06.rs) | 5780 |
198198
| 7 | [Some Assembly Required](https://adventofcode.com/2015/day/7) | [Source](src/year2015/day07.rs) | 27 |

src/year2015/day04.rs

Lines changed: 90 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,113 @@
11
//! # The Ideal Stocking Stuffer
22
//!
3-
//! This solution relies on brute forcing combinations as quickly as possible using our own internal
3+
//! This solution relies on brute forcing combinations as quickly as possible using an internal
44
//! implementation of the [`MD5`] hashing algorithm.
55
//!
6-
//! Using the [`write!`] macro to join the secret key to the number is quite slow. To speed things
7-
//! up we reuse the same `u8` buffer, incrementing digits one at a time. If a carry occurs we
8-
//! propagate from right to left. Hitting the start of the secret key means that we have
9-
//! transitioned to a new power of ten, for example from 9 to 10 or 99 to 100, so we increase the
10-
//! size of the buffer by one.
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.
1112
//!
1213
//! Interestingly the total time to solve this problem is *extremely* sensitive to the secret key
1314
//! provided as input. For example my key required ~10⁷ iterations to find the answer to part two.
1415
//! However for unit testing, I was able to randomly find a value that takes only 455 iterations,
1516
//! about 22,000 times faster!
1617
//!
1718
//! [`MD5`]: crate::util::md5
18-
//! [`write!`]: std::write
19+
//! [`format!`]: std::format
1920
use crate::util::md5::hash;
21+
use std::sync::atomic::Ordering::Relaxed;
22+
use std::sync::atomic::{AtomicBool, AtomicU32};
23+
use std::sync::mpsc::{channel, Sender};
24+
use std::sync::Arc;
25+
use std::thread;
2026

21-
pub fn parse(input: &str) -> &[u8] {
22-
input.trim().as_bytes()
23-
}
27+
type Input = (u32, u32);
2428

25-
pub fn part1(input: &[u8]) -> u32 {
26-
find(input, 0xfffff000)
29+
enum Found {
30+
First(u32),
31+
Second(u32),
2732
}
2833

29-
pub fn part2(input: &[u8]) -> u32 {
30-
find(input, 0xffffff00)
31-
}
34+
pub fn parse(input: &str) -> Input {
35+
let prefix = input.trim().to_string();
36+
let done = Arc::new(AtomicBool::new(false));
37+
let counter = Arc::new(AtomicU32::new(1000));
38+
let (tx, rx) = channel::<Found>();
3239

33-
fn find(input: &[u8], mask: u32) -> u32 {
34-
let mut count = 0;
35-
let mut size = input.len();
36-
let mut buffer = [b'0'; 32];
37-
38-
buffer[..size].copy_from_slice(input);
39-
40-
loop {
41-
count += 1;
42-
43-
let mut index = size;
44-
loop {
45-
if buffer[index] < b'9' {
46-
buffer[index] += 1;
47-
break;
48-
} else if index == input.len() {
49-
buffer[index] = b'1';
50-
size += 1;
51-
break;
40+
// Handle the first 999 numbers specially as the number of digits varies.
41+
for n in 1..1000 {
42+
let string = format!("{prefix}{n}");
43+
check_hash(string.as_bytes(), n, &tx);
44+
}
45+
46+
// Use as many cores as possible to parallelize the search.
47+
for _ in 0..thread::available_parallelism().unwrap().get() {
48+
let prefix = prefix.clone();
49+
let done = done.clone();
50+
let counter = counter.clone();
51+
let tx = tx.clone();
52+
thread::spawn(move || worker(&prefix, &done, &counter, &tx));
53+
}
54+
55+
// Explicitly drop the reference to the sender object so that when all search threads finish,
56+
// there will be no remaining references. When this happens `rx.recv` will return
57+
// `Error` and exit the loop below. This ensures we wait to receive results from all threads,
58+
// to handle the edge case where two values could be close together and found out of order.
59+
drop(tx);
60+
61+
// We could potentially find multiple values, keep only the first occurence of each one.
62+
let mut first = u32::MAX;
63+
let mut second = u32::MAX;
64+
65+
while let Ok(message) = rx.recv() {
66+
match message {
67+
Found::First(value) => {
68+
first = first.min(value);
69+
}
70+
Found::Second(value) => {
71+
second = second.min(value);
72+
done.store(true, Relaxed);
5273
}
53-
buffer[index] = b'0';
54-
index -= 1;
5574
}
75+
}
76+
77+
(first, second)
78+
}
79+
80+
pub fn part1(input: &Input) -> u32 {
81+
input.0
82+
}
83+
84+
pub fn part2(input: &Input) -> u32 {
85+
input.1
86+
}
87+
88+
fn check_hash(buffer: &[u8], n: u32, tx: &Sender<Found>) {
89+
let (result, ..) = hash(buffer);
90+
91+
if result & 0xffffff00 == 0 {
92+
let _ = tx.send(Found::Second(n));
93+
} else if result & 0xfffff000 == 0 {
94+
let _ = tx.send(Found::First(n));
95+
}
96+
}
97+
98+
fn worker(prefix: &str, done: &Arc<AtomicBool>, counter: &Arc<AtomicU32>, tx: &Sender<Found>) {
99+
while !done.load(Relaxed) {
100+
let start = counter.fetch_add(1000, Relaxed);
101+
let string = format!("{prefix}{start}");
102+
let size = string.len() - 3;
103+
let mut buffer = string.as_bytes().to_vec();
56104

57-
if hash(&buffer[..(size + 1)]).0 & mask == 0 {
58-
return count;
105+
for n in 0..1000 {
106+
// Format macro is very slow, so update digits directly
107+
buffer[size] = b'0' + (n / 100) as u8;
108+
buffer[size + 1] = b'0' + ((n / 10) % 10) as u8;
109+
buffer[size + 2] = b'0' + (n % 10) as u8;
110+
check_hash(&buffer, start + n, tx);
59111
}
60112
}
61113
}

0 commit comments

Comments
 (0)