testbench/lib.rs
1//! Testing and benchmarking tools for concurrent Rust code
2//!
3//! This crate groups together a bunch of utilities which I've found useful when
4//! testing and benchmarking Rust concurrency primitives in the triple_buffer
5//! and spmc_buffer crates.
6//!
7//! If it proves popular, other testing and benchmarking tools may be added,
8//! based on user demand.
9//!
10//! # Examples
11//!
12//! For examples of this crate at work, look at its "tests" and "benchs"
13//! submodules, which showcase expected usage.
14
15#![warn(
16 anonymous_parameters,
17 missing_copy_implementations,
18 missing_debug_implementations,
19 missing_docs,
20 nonstandard_style,
21 rust_2018_idioms,
22 single_use_lifetimes,
23 trivial_casts,
24 trivial_numeric_casts,
25 unreachable_pub,
26 unused_extern_crates,
27 unused_qualifications,
28 variant_size_differences
29)]
30
31pub mod noinline;
32pub mod race_cell;
33
34use std::sync::{
35 atomic::{AtomicBool, Ordering},
36 Barrier,
37};
38
39/// Test that running two operations concurrently works
40///
41/// When testing multi-threaded constructs, such as synchronization primitives,
42/// checking that the code works when run sequentially is insufficient.
43/// Concurrent interactions must also be tested, by running some operations
44/// in parallel across multiple threads.
45///
46/// Ideally, this function would take as input a variable-size set of functors
47/// to be run in parallel. Since Rust does not support variadic generics yet,
48/// however, multiple versions of this function must be provided, each
49/// associated with a different functor tuple size.
50///
51/// # Panics
52///
53/// This function will propagate panics from the inner functors.
54///
55pub fn concurrent_test_2(f1: impl FnOnce() + Send, f2: impl FnOnce() + Send) {
56 let barrier = Barrier::new(2);
57 std::thread::scope(|s| {
58 s.spawn(|| {
59 barrier.wait();
60 noinline::call_once(f1);
61 });
62 barrier.wait();
63 noinline::call_once(f2);
64 })
65}
66
67/// Test that running three operations concurrently works
68///
69/// This is a variant of concurrent_test_2 that works with three functors
70/// instead of two. It is hoped that future evolutions of Rust will render this
71/// (light) code duplication obsolete, in favor of some variadic design.
72///
73/// # Panics
74///
75/// This function will propagate panics from the inner functors.
76///
77pub fn concurrent_test_3(
78 f1: impl FnOnce() + Send,
79 f2: impl FnOnce() + Send,
80 f3: impl FnOnce() + Send,
81) {
82 let barrier = Barrier::new(3);
83 std::thread::scope(|s| {
84 s.spawn(|| {
85 barrier.wait();
86 noinline::call_once(f1);
87 });
88 s.spawn(|| {
89 barrier.wait();
90 noinline::call_once(f2);
91 });
92 barrier.wait();
93 noinline::call_once(f3);
94 })
95}
96
97/// Perform some operation while another is running in a loop in another thread
98///
99/// For multithreaded code, benchmarking the performance of isolated operations
100/// is usually only half of the story. Synchronization and memory contention can
101/// also have a large impact on performance.
102///
103/// For this reason, it is often useful to also measure the performance of one
104/// operation as another "antagonist" operation is also running in a background
105/// thread. This function helps you with setting up such an antagonist thread.
106///
107/// Note that the antagonist function must be designed in such a way as not to
108/// be optimized out by the compiler when run in a tight loop. Here are some
109/// ways to do this:
110///
111/// - You can hide the fact that the code is run in a loop by preventing the
112/// compiler from inlining it there, see this crate's `noinline::call_mut()`.
113/// - You can obscure the fact that inputs are always the same and outputs are
114/// are not used by using `core::hint::black_box()` on nightly Rust, or its
115/// emulation by the Criterion benchmarking crate.
116/// - You can generate inputs that the compiler cannot guess using a random
117/// number generator, and use your outputs by sending them through some sort
118/// of reduction function (sum, min, max...) and checking the result.
119///
120pub fn run_under_contention<AntagonistResult, BenchmarkResult>(
121 mut antagonist: impl FnMut() -> AntagonistResult + Send,
122 mut benchmark: impl FnMut() -> BenchmarkResult,
123) -> BenchmarkResult {
124 let start_barrier = Barrier::new(2);
125 let continue_flag = AtomicBool::new(true);
126 std::thread::scope(|s| {
127 s.spawn(|| {
128 start_barrier.wait();
129 while continue_flag.load(Ordering::Relaxed) {
130 antagonist();
131 }
132 });
133 start_barrier.wait();
134 let result = benchmark();
135 continue_flag.store(false, Ordering::Relaxed);
136 result
137 })
138}
139
140/// Examples of concurrent testing code
141#[cfg(test)]
142mod tests {
143 use std::{
144 sync::atomic::{AtomicUsize, Ordering},
145 time::Duration,
146 };
147
148 // Check the behaviour of concurrent atomic swaps and fetch-adds
149 #[test]
150 fn swap_and_fetch_add() {
151 // Amount of atomic operations to check
152 const ATOMIC_OPS_COUNT: usize = 100_000_000;
153
154 // Create a shared atomic variable
155 let atom = AtomicUsize::new(0);
156
157 // Check that concurrent atomic operations work correctly
158 let mut last_value = 0;
159 super::concurrent_test_2(
160 || {
161 // One thread continuously increments the atomic variable...
162 for _ in 1..=ATOMIC_OPS_COUNT {
163 let former_atom = atom.fetch_add(1, Ordering::Relaxed);
164 assert!((former_atom == 0) || (former_atom == last_value));
165 last_value = former_atom + 1;
166 }
167 },
168 || {
169 // ...as another continuously resets it to zero
170 for _ in 1..=ATOMIC_OPS_COUNT {
171 let former_atom = atom.swap(0, Ordering::Relaxed);
172 assert!(former_atom <= ATOMIC_OPS_COUNT);
173 }
174 },
175 );
176 }
177
178 // Check the behaviour of concurrent fetch-and/or/xors
179 #[test]
180 fn fetch_and_or_xor() {
181 // Amount of atomic operations to check
182 const ATOMIC_OPS_COUNT: usize = 30_000_000;
183
184 // Create a shared atomic variable. Even though this is an atomic Usize,
185 // we will only use the 16 low-order bits for maximal portability.
186 let atom = AtomicUsize::new(0);
187
188 // Masks used by each atomic operation
189 const AND_MASK: usize = 0b0000_0000_0000_0000; // Clear all bits
190 const XOR_MASK: usize = 0b0000_1111_0000_1111; // Flip some bits
191 const OR_MASK: usize = 0b1111_0000_1111_0000; // Set other bits
192
193 // Check that concurrent atomic operations work correctly by ensuring
194 // that at any point in time, only the 16 low-order bits can be set, and
195 // the grouped sets of bits in the masks above are either all set or
196 // all cleared in any observable state.
197 super::concurrent_test_3(
198 || {
199 // One thread runs fetch-ands in a loop...
200 for _ in 1..=ATOMIC_OPS_COUNT {
201 let old_val = atom.fetch_and(AND_MASK, Ordering::Relaxed);
202 assert_eq!(old_val & 0b1111_1111_1111_1111, old_val);
203 assert!((old_val & XOR_MASK == XOR_MASK) || (old_val & XOR_MASK == 0));
204 assert!((old_val & OR_MASK == OR_MASK) || (old_val & OR_MASK == 0));
205 }
206 },
207 || {
208 // ...another runs fetch-ors in a loop...
209 for _ in 1..=ATOMIC_OPS_COUNT {
210 let old_val = atom.fetch_or(OR_MASK, Ordering::Relaxed);
211 assert_eq!(old_val & 0b1111_1111_1111_1111, old_val);
212 assert!((old_val & XOR_MASK == XOR_MASK) || (old_val & XOR_MASK == 0));
213 assert!((old_val & OR_MASK == OR_MASK) || (old_val & OR_MASK == 0));
214 }
215 },
216 || {
217 // ...and the last one runs fetch-xors in a loop...
218 for _ in 1..=ATOMIC_OPS_COUNT {
219 let old_val = atom.fetch_xor(XOR_MASK, Ordering::Relaxed);
220 assert_eq!(old_val & 0b1111_1111_1111_1111, old_val);
221 assert!((old_val & XOR_MASK == XOR_MASK) || (old_val & XOR_MASK == 0));
222 assert!((old_val & OR_MASK == OR_MASK) || (old_val & OR_MASK == 0));
223 }
224 },
225 );
226 }
227
228 // Show how adversarial code is actually run in concurrent "benchmarking"
229 #[test]
230 fn antagonist_showcase() {
231 let atom = AtomicUsize::new(0);
232 super::run_under_contention(
233 || atom.fetch_add(1, Ordering::Relaxed),
234 || std::thread::sleep(Duration::from_millis(100)),
235 );
236 assert!(atom.load(Ordering::Relaxed) > 100000);
237 }
238}