1 | //! Types used to implement runtime argument support. |
2 | |
3 | use std::{ |
4 | any::{Any, TypeId}, |
5 | borrow::Cow, |
6 | mem, slice, |
7 | sync::OnceLock, |
8 | }; |
9 | |
10 | use crate::{util::ty::TypeCast, Bencher}; |
11 | |
12 | /// Holds lazily-initialized runtime arguments to be passed into a benchmark. |
13 | /// |
14 | /// `#[divan::bench]` stores this as a `__DIVAN_ARGS` global for each entry, and |
15 | /// then at runtime it is initialized once by a closure that creates the usable |
16 | /// `BenchArgsRunner`. |
17 | pub struct BenchArgs { |
18 | args: OnceLock<ErasedArgsSlice>, |
19 | } |
20 | |
21 | /// The result of making `BenchArgs` runnable from instantiating the arguments |
22 | /// list and providing a typed benchmarking implementation. |
23 | #[derive (Clone, Copy)] |
24 | pub struct BenchArgsRunner { |
25 | args: &'static ErasedArgsSlice, |
26 | bench: fn(Bencher, &ErasedArgsSlice, arg_index: usize), |
27 | } |
28 | |
29 | /// Type-erased `&'static [T]` that also stores names of the arguments. |
30 | struct ErasedArgsSlice { |
31 | /// The start of `&[T]`. |
32 | args: *const (), |
33 | |
34 | /// The start of `&[&'static str]`. |
35 | names: *const &'static str, |
36 | |
37 | /// The number of arguments. |
38 | len: usize, |
39 | |
40 | /// The ID of `T` to ensure correctness. |
41 | arg_type: TypeId, |
42 | } |
43 | |
44 | // SAFETY: Raw pointers in `ErasedArgsSlice` are used in a thread-safe way, and |
45 | // the argument type is required to be `Send + Sync` when initialized from the |
46 | // iterator in `BenchArgs::runner`. |
47 | unsafe impl Send for ErasedArgsSlice {} |
48 | unsafe impl Sync for ErasedArgsSlice {} |
49 | |
50 | impl BenchArgs { |
51 | /// Creates an uninitialized instance. |
52 | pub const fn new() -> Self { |
53 | Self { args: OnceLock::new() } |
54 | } |
55 | |
56 | /// Initializes `self` with the results of `make_args` and returns a |
57 | /// `BenchArgsRunner` that will execute the benchmarking closure. |
58 | pub fn runner<I, B>( |
59 | &'static self, |
60 | make_args: impl FnOnce() -> I, |
61 | arg_to_string: impl Fn(&I::Item) -> String, |
62 | _bench_impl: B, |
63 | ) -> BenchArgsRunner |
64 | where |
65 | I: IntoIterator, |
66 | I::Item: Any + Send + Sync, |
67 | B: FnOnce(Bencher, &I::Item) + Copy, |
68 | { |
69 | let args = self.args.get_or_init(|| { |
70 | let args_iter = make_args().into_iter(); |
71 | |
72 | // Reuse arguments for names if already a slice of strings. |
73 | // |
74 | // NOTE: We do this over `I::IntoIter` instead of `I` since it works |
75 | // for both slices and `slice::Iter`. |
76 | let args_strings: Option<&'static [&str]> = |
77 | args_iter.cast_ref::<slice::Iter<&str>>().map(|iter| iter.as_slice()); |
78 | |
79 | // Collect arguments into leaked slice. |
80 | // |
81 | // Leaking the collected `args` simplifies memory management, such |
82 | // as when reusing for `names`. We're leaking anyways since this is |
83 | // accessed via a global `OnceLock`. |
84 | // |
85 | // PERF: We could optimize this to reuse arguments when users |
86 | // provide slices. However, for slices its `Item` is a reference, so |
87 | // `slice::Iter<I::Item>` would never match here. To make this |
88 | // optimization, we would need to be able to get the referee type. |
89 | let args: &'static [I::Item] = Box::leak(args_iter.collect()); |
90 | |
91 | // Collect printable representations of arguments. |
92 | // |
93 | // PERF: We take multiple opportunities to reuse the provided |
94 | // arguments buffer or individual strings' buffers: |
95 | // - `&[&str]` |
96 | // - `IntoIterator<Item = &str>` |
97 | // - `IntoIterator<Item = String>` |
98 | // - `IntoIterator<Item = Box<str>>` |
99 | // - `IntoIterator<Item = Cow<str>>` |
100 | let names: &'static [&str] = 'names: { |
101 | // PERF: Reuse arguments strings slice. |
102 | if let Some(args) = args_strings { |
103 | break 'names args; |
104 | } |
105 | |
106 | // PERF: Reuse our args slice allocation. |
107 | if let Some(args) = args.cast_ref::<&[&str]>() { |
108 | break 'names args; |
109 | } |
110 | |
111 | Box::leak( |
112 | args.iter() |
113 | .map(|arg| -> &str { |
114 | // PERF: Reuse strings as-is. |
115 | if let Some(arg) = arg.cast_ref::<String>() { |
116 | return arg; |
117 | } |
118 | if let Some(arg) = arg.cast_ref::<Box<str>>() { |
119 | return arg; |
120 | } |
121 | if let Some(arg) = arg.cast_ref::<Cow<str>>() { |
122 | return arg; |
123 | } |
124 | |
125 | // Default to `arg_to_string`, which will format via |
126 | // either `ToString` or `Debug`. |
127 | Box::leak(arg_to_string(arg).into_boxed_str()) |
128 | }) |
129 | .collect(), |
130 | ) |
131 | }; |
132 | |
133 | ErasedArgsSlice { |
134 | // We `black_box` arguments to prevent the compiler from |
135 | // optimizing the benchmark for the provided values. |
136 | args: crate::black_box(args.as_ptr().cast()), |
137 | names: names.as_ptr(), |
138 | len: args.len(), |
139 | arg_type: TypeId::of::<I::Item>(), |
140 | } |
141 | }); |
142 | |
143 | BenchArgsRunner { args, bench: bench::<I::Item, B> } |
144 | } |
145 | } |
146 | |
147 | impl Default for BenchArgs { |
148 | fn default() -> Self { |
149 | Self::new() |
150 | } |
151 | } |
152 | |
153 | impl BenchArgsRunner { |
154 | #[inline ] |
155 | pub(crate) fn bench(&self, bencher: Bencher, index: usize) { |
156 | (self.bench)(bencher, self.args, index) |
157 | } |
158 | |
159 | #[inline ] |
160 | pub(crate) fn arg_names(&self) -> &'static [&'static str] { |
161 | self.args.names() |
162 | } |
163 | } |
164 | |
165 | impl ErasedArgsSlice { |
166 | /// Retrieves a slice of arguments if the type is `T`. |
167 | #[inline ] |
168 | fn typed_args<T: Any>(&self) -> Option<&[T]> { |
169 | if self.arg_type == TypeId::of::<T>() { |
170 | // SAFETY: `BenchArgs::runner` guarantees storing `len` instances. |
171 | Some(unsafe { slice::from_raw_parts(self.args.cast(), self.len) }) |
172 | } else { |
173 | None |
174 | } |
175 | } |
176 | |
177 | /// Returns the arguments' names. |
178 | /// |
179 | /// Names are in the same order as args and thus their indices can be used |
180 | /// to reference arguments. |
181 | #[inline ] |
182 | fn names(&self) -> &'static [&str] { |
183 | // SAFETY: `BenchArgs::runner` guarantees storing `len` names. |
184 | unsafe { slice::from_raw_parts(self.names, self.len) } |
185 | } |
186 | } |
187 | |
188 | /// The `BenchArgsRunner.bench` implementation. |
189 | fn bench<T, B>(bencher: Bencher, erased_args: &ErasedArgsSlice, arg_index: usize) |
190 | where |
191 | T: Any, |
192 | B: FnOnce(Bencher, &T) + Copy, |
193 | { |
194 | // We defer type checking until the benchmark is run to make safety of this |
195 | // function easier to audit. Checking here instead of in `BenchArgs::runner` |
196 | // is late but fine since this check will only fail due to a bug in Divan's |
197 | // macro code generation. |
198 | |
199 | let Some(typed_args) = erased_args.typed_args::<T>() else { |
200 | type_mismatch::<T>(); |
201 | |
202 | // Reduce code size by using a separate function for each `T` instead of |
203 | // each benchmark closure. |
204 | #[cold ] |
205 | #[inline (never)] |
206 | fn type_mismatch<T>() -> ! { |
207 | unreachable!("incorrect type ' {}'" , std::any::type_name::<T>()) |
208 | } |
209 | }; |
210 | |
211 | // SAFETY: The closure is a ZST, so we can construct one out of thin air. |
212 | // This can be done multiple times without invoking a `Drop` destructor |
213 | // because it implements `Copy`. |
214 | let bench_impl: B = unsafe { |
215 | assert_eq!(size_of::<B>(), 0, "benchmark closure expected to be zero-sized" ); |
216 | mem::zeroed() |
217 | }; |
218 | |
219 | bench_impl(bencher, &typed_args[arg_index]); |
220 | } |
221 | |
222 | #[cfg (test)] |
223 | mod tests { |
224 | use super::*; |
225 | |
226 | /// Test that optimizations for string items are applied. |
227 | mod optimizations { |
228 | use std::borrow::Borrow; |
229 | |
230 | use super::*; |
231 | |
232 | /// Tests that two slices contain the same exact strings. |
233 | fn test_eq_ptr<A: Borrow<str>, B: Borrow<str>>(a: &[A], b: &[B]) { |
234 | assert_eq!(a.len(), b.len()); |
235 | |
236 | for (a, b) in a.iter().zip(b) { |
237 | let a = a.borrow(); |
238 | let b = b.borrow(); |
239 | assert_eq!(a, b); |
240 | assert_eq!(a.as_ptr(), b.as_ptr()); |
241 | } |
242 | } |
243 | |
244 | /// Tests that `&[&str]` reuses the original slice for names. |
245 | #[test ] |
246 | fn str_slice() { |
247 | static ARGS: BenchArgs = BenchArgs::new(); |
248 | static ORIG_ARGS: &[&str] = &["a" , "b" ]; |
249 | |
250 | let runner = ARGS.runner(|| ORIG_ARGS, ToString::to_string, |_, _| {}); |
251 | |
252 | let typed_args: Vec<&str> = |
253 | runner.args.typed_args::<&&str>().unwrap().iter().copied().copied().collect(); |
254 | let names = runner.arg_names(); |
255 | |
256 | // Test values. |
257 | assert_eq!(names, ORIG_ARGS); |
258 | assert_eq!(names, typed_args); |
259 | |
260 | // Test addresses. |
261 | assert_eq!(names.as_ptr(), ORIG_ARGS.as_ptr()); |
262 | assert_ne!(names.as_ptr(), typed_args.as_ptr()); |
263 | } |
264 | |
265 | /// Tests optimizing `IntoIterator<Item = &str>` to reuse the same |
266 | /// allocation for also storing argument names. |
267 | #[test ] |
268 | fn str_array() { |
269 | static ARGS: BenchArgs = BenchArgs::new(); |
270 | |
271 | let runner = ARGS.runner(|| ["a" , "b" ], ToString::to_string, |_, _| {}); |
272 | |
273 | let typed_args = runner.args.typed_args::<&str>().unwrap(); |
274 | let names = runner.arg_names(); |
275 | |
276 | // Test values. |
277 | assert_eq!(names, ["a" , "b" ]); |
278 | assert_eq!(names, typed_args); |
279 | |
280 | // Test addresses. |
281 | assert_eq!(names.as_ptr(), typed_args.as_ptr()); |
282 | } |
283 | |
284 | /// Tests optimizing `IntoIterator<Item = String>` to reuse the same |
285 | /// allocation for also storing argument names. |
286 | #[test ] |
287 | fn string_array() { |
288 | static ARGS: BenchArgs = BenchArgs::new(); |
289 | |
290 | let runner = |
291 | ARGS.runner(|| ["a" .to_owned(), "b" .to_owned()], ToString::to_string, |_, _| {}); |
292 | |
293 | let typed_args = runner.args.typed_args::<String>().unwrap(); |
294 | let names = runner.arg_names(); |
295 | |
296 | assert_eq!(names, ["a" , "b" ]); |
297 | test_eq_ptr(names, typed_args); |
298 | } |
299 | |
300 | /// Tests optimizing `IntoIterator<Item = Box<str>>` to reuse the same |
301 | /// allocation for also storing argument names. |
302 | #[test ] |
303 | fn box_str_array() { |
304 | static ARGS: BenchArgs = BenchArgs::new(); |
305 | |
306 | let runner = ARGS.runner( |
307 | || ["a" .to_owned().into_boxed_str(), "b" .to_owned().into_boxed_str()], |
308 | ToString::to_string, |
309 | |_, _| {}, |
310 | ); |
311 | |
312 | let typed_args = runner.args.typed_args::<Box<str>>().unwrap(); |
313 | let names = runner.arg_names(); |
314 | |
315 | assert_eq!(names, ["a" , "b" ]); |
316 | test_eq_ptr(names, typed_args); |
317 | } |
318 | |
319 | /// Tests optimizing `IntoIterator<Item = Cow<str>>` to reuse the same |
320 | /// allocation for also storing argument names. |
321 | #[test ] |
322 | fn cow_str_array() { |
323 | static ARGS: BenchArgs = BenchArgs::new(); |
324 | |
325 | let runner = ARGS.runner( |
326 | || [Cow::Owned("a" .to_owned()), Cow::Borrowed("b" )], |
327 | ToString::to_string, |
328 | |_, _| {}, |
329 | ); |
330 | |
331 | let typed_args = runner.args.typed_args::<Cow<str>>().unwrap(); |
332 | let names = runner.arg_names(); |
333 | |
334 | assert_eq!(names, ["a" , "b" ]); |
335 | test_eq_ptr(names, typed_args); |
336 | } |
337 | } |
338 | } |
339 | |