Skip to main content

fluxbench_core/bencher/
batched.rs

1//! Batched `iter*` methods: untimed setup builds inputs, one timer covers each batch.
2
3use super::batch_size::BatchSize;
4use super::engine::SampleRaw;
5use super::state::Bencher;
6use crate::measure::Timer;
7use std::hint::black_box;
8
9impl Bencher {
10    /// Measure `routine` on fresh inputs from `setup`. Setup time is excluded.
11    ///
12    /// Same as `iter_batched(setup, routine, BatchSize::SmallInput)`.
13    #[inline]
14    pub fn iter_with_setup<T, S, F, R>(&mut self, setup: S, routine: F)
15    where
16        S: FnMut() -> T,
17        F: FnMut(T) -> R,
18    {
19        self.iter_batched(setup, routine, BatchSize::SmallInput);
20    }
21
22    /// Measure `routine` on inputs it takes by value. Setup time is excluded.
23    ///
24    /// `setup` runs once per call. It builds each batch of inputs untimed.
25    /// `size` sets the inputs per batch. One timer covers the routine over a batch.
26    /// Outputs drop after the timer stops.
27    #[inline]
28    pub fn iter_batched<T, S, F, R>(&mut self, setup: S, mut routine: F, size: BatchSize)
29    where
30        S: FnMut() -> T,
31        F: FnMut(T) -> R,
32    {
33        self.run_batched(size, setup, |inputs: &mut Vec<T>, outputs: &mut Vec<R>| {
34            for input in inputs.drain(..) {
35                outputs.push(black_box(routine(input)));
36            }
37        });
38    }
39
40    /// Measure `routine` on inputs it borrows mutably. Setup time is excluded.
41    ///
42    /// Same batching as `iter_batched`.
43    /// Inputs and outputs drop after the timer stops.
44    #[inline]
45    pub fn iter_batched_ref<T, S, F, R>(&mut self, setup: S, mut routine: F, size: BatchSize)
46    where
47        S: FnMut() -> T,
48        F: FnMut(&mut T) -> R,
49    {
50        self.run_batched(size, setup, |inputs: &mut Vec<T>, outputs: &mut Vec<R>| {
51            for input in inputs.iter_mut() {
52                outputs.push(black_box(routine(input)));
53            }
54        });
55    }
56
57    /// Measure `routine` on a shared input that `setup` rebuilds every `batch_size` calls.
58    ///
59    /// `batch_size` sets only the setup cadence. A value of 0 rises to 1.
60    /// Each sample calls `setup` `ceil(iters_per_sample / batch_size)` times, untimed.
61    #[inline]
62    pub fn iter_batched_shared<T, S, F, R>(&mut self, batch_size: u64, mut setup: S, mut routine: F)
63    where
64        S: FnMut() -> T,
65        F: FnMut(&T) -> R,
66    {
67        if !self.begin_iter() {
68            return;
69        }
70        let batch_size = batch_size.max(1);
71        self.run_loop(|b, n| {
72            let mut raw = SampleRaw::default();
73            let mut remaining = n;
74            while remaining > 0 {
75                let chunk = remaining.min(batch_size);
76                let input = setup();
77
78                b.alloc_reset();
79                let timer = Timer::start();
80                for _ in 0..chunk {
81                    black_box(routine(black_box(&input)));
82                }
83                let (ns, cycles) = timer.stop();
84                raw.add(ns, cycles, b.alloc_read());
85
86                drop(input);
87                remaining -= chunk;
88            }
89            raw
90        });
91    }
92
93    /// Run warmup and measurement in batches of `size` inputs.
94    ///
95    /// Per batch: `setup` fills `inputs` untimed, `timed` runs under one timer, then both buffers clear.
96    /// `timed` consumes or borrows the inputs and pushes one output per input.
97    /// The buffers live across batches, so their capacity is reused.
98    fn run_batched<T, R, S, B>(&mut self, size: BatchSize, mut setup: S, mut timed: B)
99    where
100        S: FnMut() -> T,
101        B: FnMut(&mut Vec<T>, &mut Vec<R>),
102    {
103        if !self.begin_iter() {
104            return;
105        }
106        let mut inputs: Vec<T> = Vec::new();
107        let mut outputs: Vec<R> = Vec::new();
108        self.run_loop(|b, n| {
109            let mut raw = SampleRaw::default();
110            let batch_len = size.batch_len(n);
111            let mut remaining = n;
112            while remaining > 0 {
113                let chunk = remaining.min(batch_len);
114                // `chunk` always fits `usize` on 64-bit targets. Elsewhere it saturates.
115                let len = usize::try_from(chunk).unwrap_or(usize::MAX);
116                inputs.reserve(len);
117                outputs.reserve(len);
118                inputs.extend((0..chunk).map(|_| setup()));
119
120                b.alloc_reset();
121                let timer = Timer::start();
122                timed(&mut inputs, &mut outputs);
123                let (ns, cycles) = timer.stop();
124                raw.add(ns, cycles, b.alloc_read());
125
126                outputs.clear();
127                inputs.clear();
128                remaining -= chunk;
129            }
130            raw
131        });
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use crate::bencher::test_support::{fixed, quick};
138    use crate::bencher::{BatchSize, Bencher, MeasureConfig, run_benchmark_loop};
139    use std::cell::Cell;
140    use std::time::Duration;
141
142    /// Counts setup calls and batches. A batch starts at a setup that follows a routine call.
143    #[derive(Default)]
144    struct Trace {
145        setups: Cell<u64>,
146        calls: Cell<u64>,
147        batches: Cell<u64>,
148        in_setup: Cell<bool>,
149    }
150
151    impl Trace {
152        fn setup(&self) {
153            if !self.in_setup.replace(true) {
154                self.batches.set(self.batches.get() + 1);
155            }
156            self.setups.set(self.setups.get() + 1);
157        }
158
159        fn call(&self) {
160            self.in_setup.set(false);
161            self.calls.set(self.calls.get() + 1);
162        }
163    }
164
165    /// Run `iter_batched` with `size` over 2 samples of 40 calls. Returns the trace.
166    fn trace_by_value(size: BatchSize) -> Trace {
167        let trace = Trace::default();
168        let result = run_benchmark_loop(Bencher::with_config(fixed(2, 40)), |b| {
169            b.iter_batched(|| trace.setup(), |()| trace.call(), size);
170        })
171        .expect("measurement succeeds");
172        assert_eq!(result.iterations, 80);
173        trace
174    }
175
176    #[test]
177    fn setup_runs_once_per_call_for_every_batch_size() {
178        for size in [
179            BatchSize::SmallInput,
180            BatchSize::LargeInput,
181            BatchSize::PerIteration,
182            BatchSize::NumBatches(4),
183            BatchSize::NumIterations(10),
184        ] {
185            let trace = trace_by_value(size);
186            assert_eq!(trace.setups.get(), 80, "{size:?}");
187            assert_eq!(trace.calls.get(), 80, "{size:?}");
188        }
189    }
190
191    #[test]
192    fn batch_count_follows_batch_size() {
193        // Two samples of 40 calls each.
194        let cases = [
195            (BatchSize::SmallInput, 2),
196            (BatchSize::LargeInput, 2 * 3),
197            (BatchSize::PerIteration, 80),
198            (BatchSize::NumBatches(4), 2 * 4),
199            (BatchSize::NumIterations(10), 2 * 4),
200            (BatchSize::NumIterations(15), 2 * 3),
201        ];
202        for (size, batches) in cases {
203            assert_eq!(trace_by_value(size).batches.get(), batches, "{size:?}");
204        }
205    }
206
207    #[test]
208    fn iter_batched_ref_uses_the_same_batches() {
209        let trace = Trace::default();
210        run_benchmark_loop(Bencher::with_config(fixed(2, 40)), |b| {
211            b.iter_batched_ref(|| trace.setup(), |_| trace.call(), BatchSize::LargeInput);
212        })
213        .expect("measurement succeeds");
214        assert_eq!(trace.setups.get(), 80);
215        assert_eq!(trace.batches.get(), 6);
216    }
217
218    #[test]
219    fn iter_batched_excludes_setup_time() {
220        let result = run_benchmark_loop(Bencher::with_config(fixed(2, 5)), |b| {
221            b.iter_batched(
222                || {
223                    std::thread::sleep(Duration::from_millis(1));
224                    vec![1_u64, 2, 3]
225                },
226                |v| v.iter().sum::<u64>(),
227                BatchSize::SmallInput,
228            );
229        })
230        .expect("measurement succeeds");
231        assert!(
232            result.samples.iter().all(|s| s.duration_nanos < 500_000.0),
233            "samples: {:?}",
234            result.samples
235        );
236    }
237
238    /// Sleeps 1 ms on drop. A drop inside the timer adds about 1 ms per call.
239    struct SlowDrop;
240
241    impl Drop for SlowDrop {
242        fn drop(&mut self) {
243            std::thread::sleep(Duration::from_millis(1));
244        }
245    }
246
247    #[test]
248    fn iter_batched_drops_outputs_outside_the_timer() {
249        let result = run_benchmark_loop(Bencher::with_config(fixed(2, 5)), |b| {
250            b.iter_batched(|| (), |()| SlowDrop, BatchSize::SmallInput);
251        })
252        .expect("measurement succeeds");
253        assert!(
254            result.samples.iter().all(|s| s.duration_nanos < 500_000.0),
255            "samples: {:?}",
256            result.samples
257        );
258    }
259
260    #[test]
261    fn iter_batched_ref_drops_inputs_and_outputs_outside_the_timer() {
262        let result = run_benchmark_loop(Bencher::with_config(fixed(2, 5)), |b| {
263            b.iter_batched_ref(|| SlowDrop, |_| SlowDrop, BatchSize::PerIteration);
264        })
265        .expect("measurement succeeds");
266        assert!(
267            result.samples.iter().all(|s| s.duration_nanos < 500_000.0),
268            "samples: {:?}",
269            result.samples
270        );
271    }
272
273    /// Adds its final `value` to `total` on drop.
274    struct Probe<'a> {
275        value: u64,
276        total: &'a Cell<u64>,
277    }
278
279    impl Drop for Probe<'_> {
280        fn drop(&mut self) {
281            self.total.set(self.total.get() + self.value);
282        }
283    }
284
285    #[test]
286    fn iter_batched_ref_mutation_is_visible() {
287        let total = Cell::new(0_u64);
288        let result = run_benchmark_loop(Bencher::with_config(fixed(3, 7)), |b| {
289            b.iter_batched_ref(
290                || Probe {
291                    value: 0,
292                    total: &total,
293                },
294                |probe| {
295                    probe.value += 2;
296                    probe.value
297                },
298                BatchSize::NumBatches(2),
299            );
300        })
301        .expect("measurement succeeds");
302        assert_eq!(result.iterations, 21);
303        // Each input starts at 0. The routine adds 2 before the input drops.
304        assert_eq!(total.get(), 42);
305    }
306
307    #[test]
308    fn iter_batched_shared_setup_cadence() {
309        let setups = Cell::new(0_u64);
310        let calls = Cell::new(0_u64);
311        let result = run_benchmark_loop(Bencher::with_config(fixed(4, 10)), |b| {
312            b.iter_batched_shared(
313                3,
314                || setups.set(setups.get() + 1),
315                |_| calls.set(calls.get() + 1),
316            );
317        })
318        .expect("measurement succeeds");
319
320        assert_eq!(result.iterations, 40);
321        assert_eq!(calls.get(), 40);
322        // ceil(10 / 3) = 4 setups per sample.
323        assert_eq!(setups.get(), 4 * 10_u64.div_ceil(3));
324    }
325
326    #[test]
327    fn iter_batched_shared_zero_batch_size() {
328        let setups = Cell::new(0_u64);
329        let calls = Cell::new(0_u64);
330        let result = run_benchmark_loop(Bencher::with_config(fixed(2, 5)), |b| {
331            b.iter_batched_shared(
332                0,
333                || setups.set(setups.get() + 1),
334                |_| calls.set(calls.get() + 1),
335            );
336        })
337        .expect("measurement succeeds");
338
339        assert_eq!(result.iterations, 10);
340        assert_eq!(calls.get(), 10);
341        assert_eq!(setups.get(), 10);
342    }
343
344    #[test]
345    fn iter_with_setup_excludes_setup_time() {
346        let setups = Cell::new(0_u64);
347        let result = run_benchmark_loop(Bencher::with_config(fixed(2, 5)), |b| {
348            b.iter_with_setup(
349                || {
350                    setups.set(setups.get() + 1);
351                    std::thread::sleep(Duration::from_millis(1));
352                    vec![1_u64, 2, 3]
353                },
354                |v| v.iter().sum::<u64>(),
355            );
356        })
357        .expect("measurement succeeds");
358
359        assert_eq!(setups.get(), 10);
360        assert_eq!(result.iterations, 10);
361        // Each setup sleeps 1 ms. The routine takes far less.
362        assert!(
363            result.samples.iter().all(|s| s.duration_nanos < 500_000.0),
364            "samples: {:?}",
365            result.samples
366        );
367    }
368
369    #[test]
370    fn slow_setup_keeps_every_sample() {
371        let config = MeasureConfig {
372            warmup_ns: 5_000_000,
373            measurement_ns: 50_000_000,
374            ..quick(10)
375        };
376        let result = run_benchmark_loop(Bencher::with_config(config), |b| {
377            b.iter_with_setup(|| std::thread::sleep(Duration::from_millis(1)), |()| 1_u64);
378        })
379        .expect("measurement succeeds");
380
381        // Wall-clock sizing counts the 1 ms setup: about 5 calls per 5 ms sample.
382        assert_eq!(result.samples.len(), 10);
383        assert!(!result.truncated);
384        assert!(
385            result.iters_per_sample <= 10,
386            "ips: {}",
387            result.iters_per_sample
388        );
389    }
390
391    #[test]
392    fn iter_with_setup_chunks_large_batches() {
393        let trace = Trace::default();
394        let result = run_benchmark_loop(Bencher::with_config(fixed(2, 2_500)), |b| {
395            b.iter_with_setup(|| trace.setup(), |()| trace.call());
396        })
397        .expect("measurement succeeds");
398
399        assert_eq!(result.iterations, 5_000);
400        assert_eq!(trace.setups.get(), 5_000);
401        assert_eq!(trace.calls.get(), 5_000);
402        // ceil(2500 / 1024) = 3 chunks per sample.
403        assert_eq!(trace.batches.get(), 6);
404    }
405}