Skip to content

Commit 76e8437

Browse files
committed
walk: Use unbounded channels
We originally switched to bounded channels for backpressure to fix sharkdp#918. However, bounded channels have a significant initialization overhead as they pre-allocate a fixed-size buffer for the messages. This implementation uses a different backpressure strategy: each thread gets a limited-size pool of WorkerResults. When the size limit is hit, the sender thread has to wait for the receiver thread to handle a result from that pool and recycle it. Inspired by [snmalloc], results are recycled by sending the boxed result over a channel back to the thread that allocated it. By allocating and freeing each WorkerResult from the same thread, allocator contention is reduced dramatically. And since we now pass results by pointer instead of by value, message passing overhead is reduced as well. Fixes sharkdp#1408. [snmalloc]: https://github.com/microsoft/snmalloc
1 parent 70126ca commit 76e8437

File tree

2 files changed

+107
-31
lines changed

2 files changed

+107
-31
lines changed

src/exec/job.rs

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,17 @@ use std::sync::Mutex;
33
use crossbeam_channel::Receiver;
44

55
use crate::config::Config;
6-
use crate::dir_entry::DirEntry;
76
use crate::error::print_error;
87
use crate::exit_codes::{merge_exitcodes, ExitCode};
9-
use crate::walk::WorkerResult;
8+
use crate::walk::{WorkerMsg, WorkerResult};
109

1110
use super::CommandSet;
1211

1312
/// An event loop that listens for inputs from the `rx` receiver. Each received input will
1413
/// generate a command with the supplied command template. The generated command will then
1514
/// be executed, and this process will continue until the receiver's sender has closed.
1615
pub fn job(
17-
rx: Receiver<WorkerResult>,
16+
rx: Receiver<WorkerMsg>,
1817
cmd: &CommandSet,
1918
out_perm: &Mutex<()>,
2019
config: &Config,
@@ -26,7 +25,8 @@ pub fn job(
2625
loop {
2726
// Obtain the next result from the receiver, else if the channel
2827
// has closed, exit from the loop
29-
let dir_entry: DirEntry = match rx.recv() {
28+
let result = rx.recv().map(WorkerMsg::take);
29+
let dir_entry = match result {
3030
Ok(WorkerResult::Entry(dir_entry)) => dir_entry,
3131
Ok(WorkerResult::Error(err)) => {
3232
if config.show_filesystem_errors {
@@ -49,18 +49,19 @@ pub fn job(
4949
merge_exitcodes(results)
5050
}
5151

52-
pub fn batch(rx: Receiver<WorkerResult>, cmd: &CommandSet, config: &Config) -> ExitCode {
53-
let paths = rx
54-
.into_iter()
55-
.filter_map(|worker_result| match worker_result {
56-
WorkerResult::Entry(dir_entry) => Some(dir_entry.into_stripped_path(config)),
57-
WorkerResult::Error(err) => {
58-
if config.show_filesystem_errors {
59-
print_error(err.to_string());
52+
pub fn batch(rx: Receiver<WorkerMsg>, cmd: &CommandSet, config: &Config) -> ExitCode {
53+
let paths =
54+
rx.into_iter()
55+
.map(WorkerMsg::take)
56+
.filter_map(|worker_result| match worker_result {
57+
WorkerResult::Entry(dir_entry) => Some(dir_entry.into_stripped_path(config)),
58+
WorkerResult::Error(err) => {
59+
if config.show_filesystem_errors {
60+
print_error(err.to_string());
61+
}
62+
None
6063
}
61-
None
62-
}
63-
});
64+
});
6465

6566
cmd.execute_batch(paths, config.batch_size, config.path_separator.as_deref())
6667
}

src/walk.rs

Lines changed: 91 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use std::thread;
99
use std::time::{Duration, Instant};
1010

1111
use anyhow::{anyhow, Result};
12-
use crossbeam_channel::{bounded, Receiver, RecvTimeoutError, Sender};
12+
use crossbeam_channel::{unbounded, Receiver, RecvTimeoutError, Sender};
1313
use etcetera::BaseStrategy;
1414
use ignore::overrides::{Override, OverrideBuilder};
1515
use ignore::{self, WalkBuilder, WalkParallel, WalkState};
@@ -43,6 +43,77 @@ pub enum WorkerResult {
4343
Error(ignore::Error),
4444
}
4545

46+
/// Storage for a WorkerResult.
47+
type ResultBox = Box<Option<WorkerResult>>;
48+
49+
/// A WorkerResult that recycles itself.
50+
pub struct WorkerMsg {
51+
inner: Option<ResultBox>,
52+
tx: Sender<ResultBox>,
53+
}
54+
55+
impl WorkerMsg {
56+
/// Create a new message.
57+
fn new(inner: ResultBox, tx: Sender<ResultBox>) -> Self {
58+
Self {
59+
inner: Some(inner),
60+
tx,
61+
}
62+
}
63+
64+
/// Extract the result from this message.
65+
pub fn take(mut self) -> WorkerResult {
66+
self.inner.as_mut().unwrap().take().unwrap()
67+
}
68+
}
69+
70+
impl Drop for WorkerMsg {
71+
fn drop(&mut self) {
72+
let _ = self.tx.send(self.inner.take().unwrap());
73+
}
74+
}
75+
76+
/// A pool of WorkerResults that can be recycled.
77+
struct ResultPool {
78+
size: usize,
79+
tx: Sender<ResultBox>,
80+
rx: Receiver<ResultBox>,
81+
}
82+
83+
/// Capacity was chosen empircally to perform similarly to an unbounded channel
84+
const RESULT_POOL_CAPACITY: usize = 0x4000;
85+
86+
impl ResultPool {
87+
/// Create an empty pool.
88+
fn new() -> Self {
89+
let (tx, rx) = unbounded();
90+
91+
Self { size: 0, tx, rx }
92+
}
93+
94+
/// Allocate or recycle a WorkerResult from the pool.
95+
fn get(&mut self, result: WorkerResult) -> WorkerMsg {
96+
let inner = if self.size < RESULT_POOL_CAPACITY {
97+
match self.rx.try_recv() {
98+
Ok(mut inner) => {
99+
*inner = Some(result);
100+
inner
101+
}
102+
Err(_) => {
103+
self.size += 1;
104+
Box::new(Some(result))
105+
}
106+
}
107+
} else {
108+
let mut inner = self.rx.recv().unwrap();
109+
*inner = Some(result);
110+
inner
111+
};
112+
113+
WorkerMsg::new(inner, self.tx.clone())
114+
}
115+
}
116+
46117
/// Maximum size of the output buffer before flushing results to the console
47118
const MAX_BUFFER_LENGTH: usize = 1000;
48119
/// Default duration until output buffering switches to streaming.
@@ -56,8 +127,8 @@ struct ReceiverBuffer<'a, W> {
56127
quit_flag: &'a AtomicBool,
57128
/// The ^C notifier.
58129
interrupt_flag: &'a AtomicBool,
59-
/// Receiver for worker results.
60-
rx: Receiver<WorkerResult>,
130+
/// Receiver for worker messages.
131+
rx: Receiver<WorkerMsg>,
61132
/// Standard output.
62133
stdout: W,
63134
/// The current buffer mode.
@@ -72,7 +143,7 @@ struct ReceiverBuffer<'a, W> {
72143

73144
impl<'a, W: Write> ReceiverBuffer<'a, W> {
74145
/// Create a new receiver buffer.
75-
fn new(state: &'a WorkerState, rx: Receiver<WorkerResult>, stdout: W) -> Self {
146+
fn new(state: &'a WorkerState, rx: Receiver<WorkerMsg>, stdout: W) -> Self {
76147
let config = &state.config;
77148
let quit_flag = state.quit_flag.as_ref();
78149
let interrupt_flag = state.interrupt_flag.as_ref();
@@ -104,7 +175,7 @@ impl<'a, W: Write> ReceiverBuffer<'a, W> {
104175

105176
/// Receive the next worker result.
106177
fn recv(&self) -> Result<WorkerResult, RecvTimeoutError> {
107-
match self.mode {
178+
let result = match self.mode {
108179
ReceiverMode::Buffering => {
109180
// Wait at most until we should switch to streaming
110181
self.rx.recv_deadline(self.deadline)
@@ -113,7 +184,8 @@ impl<'a, W: Write> ReceiverBuffer<'a, W> {
113184
// Wait however long it takes for a result
114185
Ok(self.rx.recv()?)
115186
}
116-
}
187+
};
188+
result.map(WorkerMsg::take)
117189
}
118190

119191
/// Wait for a result or state change.
@@ -319,7 +391,7 @@ impl WorkerState {
319391

320392
/// Run the receiver work, either on this thread or a pool of background
321393
/// threads (for --exec).
322-
fn receive(&self, rx: Receiver<WorkerResult>) -> ExitCode {
394+
fn receive(&self, rx: Receiver<WorkerMsg>) -> ExitCode {
323395
let config = &self.config;
324396

325397
// This will be set to `Some` if the `--exec` argument was supplied.
@@ -355,12 +427,13 @@ impl WorkerState {
355427
}
356428

357429
/// Spawn the sender threads.
358-
fn spawn_senders(&self, walker: WalkParallel, tx: Sender<WorkerResult>) {
430+
fn spawn_senders(&self, walker: WalkParallel, tx: Sender<WorkerMsg>) {
359431
walker.run(|| {
360432
let patterns = &self.patterns;
361433
let config = &self.config;
362434
let quit_flag = self.quit_flag.as_ref();
363435
let tx = tx.clone();
436+
let mut pool = ResultPool::new();
364437

365438
Box::new(move |entry| {
366439
if quit_flag.load(Ordering::Relaxed) {
@@ -387,20 +460,22 @@ impl WorkerState {
387460
DirEntry::broken_symlink(path)
388461
}
389462
_ => {
390-
return match tx.send(WorkerResult::Error(ignore::Error::WithPath {
463+
let result = pool.get(WorkerResult::Error(ignore::Error::WithPath {
391464
path,
392465
err: inner_err,
393-
})) {
466+
}));
467+
return match tx.send(result) {
394468
Ok(_) => WalkState::Continue,
395469
Err(_) => WalkState::Quit,
396-
}
470+
};
397471
}
398472
},
399473
Err(err) => {
400-
return match tx.send(WorkerResult::Error(err)) {
474+
let result = pool.get(WorkerResult::Error(err));
475+
return match tx.send(result) {
401476
Ok(_) => WalkState::Continue,
402477
Err(_) => WalkState::Quit,
403-
}
478+
};
404479
}
405480
};
406481

@@ -509,7 +584,8 @@ impl WorkerState {
509584
}
510585
}
511586

512-
let send_result = tx.send(WorkerResult::Entry(entry));
587+
let result = pool.get(WorkerResult::Entry(entry));
588+
let send_result = tx.send(result);
513589

514590
if send_result.is_err() {
515591
return WalkState::Quit;
@@ -545,8 +621,7 @@ impl WorkerState {
545621
.unwrap();
546622
}
547623

548-
// Channel capacity was chosen empircally to perform similarly to an unbounded channel
549-
let (tx, rx) = bounded(0x4000 * config.threads);
624+
let (tx, rx) = unbounded();
550625

551626
let exit_code = thread::scope(|scope| {
552627
// Spawn the receiver thread(s)

0 commit comments

Comments
 (0)