ignore: Avoid contention on num_pending

Previously, every worker would increment the shared num_pending count on
every new work item, and decrement it after finishing them, leading to
lots of contention.  Now, we only track the number of workers actively
running, so there is no contention except when workers go to sleep or
wake up.

Closes #2642
This commit is contained in:
Tavian Barnes 2023-10-30 15:56:08 -04:00 committed by Andrew Gallant
parent af55fc2b38
commit 6d7550d58e
2 changed files with 22 additions and 25 deletions

View File

@ -23,6 +23,8 @@ Performance improvements:
Make most searches with `\b` look-arounds (among others) much faster. Make most searches with `\b` look-arounds (among others) much faster.
* [PERF #2591](https://github.com/BurntSushi/ripgrep/pull/2591): * [PERF #2591](https://github.com/BurntSushi/ripgrep/pull/2591):
Parallel directory traversal now uses work stealing for faster searches. Parallel directory traversal now uses work stealing for faster searches.
* [PERF #2642](https://github.com/BurntSushi/ripgrep/pull/2642):
Parallel directory traversal has some contention reduced.
Feature enhancements: Feature enhancements:

View File

@ -1279,7 +1279,7 @@ impl WalkParallel {
} }
// Create the workers and then wait for them to finish. // Create the workers and then wait for them to finish.
let quit_now = Arc::new(AtomicBool::new(false)); let quit_now = Arc::new(AtomicBool::new(false));
let num_pending = Arc::new(AtomicUsize::new(stack.len())); let active_workers = Arc::new(AtomicUsize::new(threads));
let stacks = Stack::new_for_each_thread(threads, stack); let stacks = Stack::new_for_each_thread(threads, stack);
std::thread::scope(|s| { std::thread::scope(|s| {
let handles: Vec<_> = stacks let handles: Vec<_> = stacks
@ -1288,7 +1288,7 @@ impl WalkParallel {
visitor: builder.build(), visitor: builder.build(),
stack, stack,
quit_now: quit_now.clone(), quit_now: quit_now.clone(),
num_pending: num_pending.clone(), active_workers: active_workers.clone(),
max_depth: self.max_depth, max_depth: self.max_depth,
max_filesize: self.max_filesize, max_filesize: self.max_filesize,
follow_links: self.follow_links, follow_links: self.follow_links,
@ -1471,8 +1471,8 @@ struct Worker<'s> {
/// that we need this because we don't want other `Work` to be done after /// that we need this because we don't want other `Work` to be done after
/// we quit. We wouldn't need this if have a priority channel. /// we quit. We wouldn't need this if have a priority channel.
quit_now: Arc<AtomicBool>, quit_now: Arc<AtomicBool>,
/// The number of outstanding work items. /// The number of currently active workers.
num_pending: Arc<AtomicUsize>, active_workers: Arc<AtomicUsize>,
/// The maximum depth of directories to descend. A value of `0` means no /// The maximum depth of directories to descend. A value of `0` means no
/// descension at all. /// descension at all.
max_depth: Option<usize>, max_depth: Option<usize>,
@ -1500,7 +1500,6 @@ impl<'s> Worker<'s> {
if let WalkState::Quit = self.run_one(work) { if let WalkState::Quit = self.run_one(work) {
self.quit_now(); self.quit_now();
} }
self.work_done();
} }
} }
@ -1682,23 +1681,20 @@ impl<'s> Worker<'s> {
return None; return None;
} }
None => { None => {
// Once num_pending reaches 0, it is impossible for it to if self.deactivate_worker() == 0 {
// ever increase again. Namely, it only reaches 0 once // If deactivate_worker() returns 0, every worker thread
// all jobs have run such that no jobs have produced more // is currently within the critical section between the
// work. We have this guarantee because num_pending is // acquire in deactivate_worker() and the release in
// always incremented before each job is submitted and only // activate_worker() below. For this to happen, every
// decremented once each job is completely finished. // worker's local deque must be simultaneously empty,
// Therefore, if this reaches zero, then there can be no // meaning there is no more work left at all.
// other job running.
if self.num_pending() == 0 {
// Every other thread is blocked at the next recv().
// Send the initial quit message and quit.
self.send_quit(); self.send_quit();
return None; return None;
} }
// Wait for next `Work` or `Quit` message. // Wait for next `Work` or `Quit` message.
loop { loop {
if let Some(v) = self.recv() { if let Some(v) = self.recv() {
self.activate_worker();
value = Some(v); value = Some(v);
break; break;
} }
@ -1724,14 +1720,8 @@ impl<'s> Worker<'s> {
self.quit_now.load(AtomicOrdering::SeqCst) self.quit_now.load(AtomicOrdering::SeqCst)
} }
/// Returns the number of pending jobs.
fn num_pending(&self) -> usize {
self.num_pending.load(AtomicOrdering::SeqCst)
}
/// Send work. /// Send work.
fn send(&self, work: Work) { fn send(&self, work: Work) {
self.num_pending.fetch_add(1, AtomicOrdering::SeqCst);
self.stack.push(Message::Work(work)); self.stack.push(Message::Work(work));
} }
@ -1745,9 +1735,14 @@ impl<'s> Worker<'s> {
self.stack.pop() self.stack.pop()
} }
/// Signal that work has been finished. /// Deactivates a worker and returns the number of currently active workers.
fn work_done(&self) { fn deactivate_worker(&self) -> usize {
self.num_pending.fetch_sub(1, AtomicOrdering::SeqCst); self.active_workers.fetch_sub(1, AtomicOrdering::Acquire) - 1
}
/// Reactivates a worker.
fn activate_worker(&self) {
self.active_workers.fetch_add(1, AtomicOrdering::Release);
} }
} }