Skip to content

Commit

Permalink
Reuse &[&str] slice for args names
Browse files Browse the repository at this point in the history
Also improved comments around these types of optimizations.
  • Loading branch information
nvzqz committed Jun 30, 2024
1 parent 8db2763 commit a2bcced
Show file tree
Hide file tree
Showing 2 changed files with 76 additions and 8 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ Versioning](http://semver.org/spec/v2.0.0.html).

### Changes

- Internally reuse [`&[&str]` slice][slice] for [`args`] names.

- Subtract overhead of [`AllocProfiler`] from timings. Now that Divan also
tracks the maximum bytes allocated, the overhead was apparent in timings.

Expand Down Expand Up @@ -327,6 +329,7 @@ Initial release. See [blog post](https://nikolaivazquez.com/blog/divan/).
[`ToString`]: https://doc.rust-lang.org/std/string/trait.ToString.html
[available parallelism]: https://doc.rust-lang.org/std/thread/fn.available_parallelism.html
[drop_fn]: https://doc.rust-lang.org/std/mem/fn.drop.html
[slice]: https://doc.rust-lang.org/std/primitive.slice.html
[`thread_local!`]: https://doc.rust-lang.org/std/macro.thread_local.html

[`pthread_key_create`]: https://pubs.opengroup.org/onlinepubs/9699919799/functions/pthread_key_create.html
81 changes: 73 additions & 8 deletions src/bench/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,20 +67,51 @@ impl BenchArgs {
B: FnOnce(Bencher, &I::Item) + Copy,
{
let args = self.args.get_or_init(|| {
// Collect arguments into a deduplicated leaked slice.
let args: &'static [I::Item] = Box::leak(make_args().into_iter().collect());
let args_iter = make_args().into_iter();

// Reuse arguments for names if already a slice of strings.
//
// NOTE: We do this over `I::IntoIter` instead of `I` since it works
// for both slices and `slice::Iter`.
let args_strings: Option<&'static [&str]> =
args_iter.cast_ref::<slice::Iter<&str>>().map(|iter| iter.as_slice());

// Collect arguments into leaked slice.
//
// Leaking the collected `args` simplifies memory management, such
// as when reusing for `names`. We're leaking anyways since this is
// accessed via a global `OnceLock`.
//
// PERF: We could optimize this to reuse arguments when users
// provide slices. However, for slices its `Item` is a reference, so
// `slice::Iter<I::Item>` would never match here. To make this
// optimization, we would need to be able to get the referee type.
let args: &'static [I::Item] = Box::leak(args_iter.collect());

// Collect printable representations of arguments.
//
// PERF: We take multiple opportunities to reuse the provided
// arguments buffer or individual strings' buffers:
// - `&[&str]`
// - `IntoIterator<Item = &str>`
// - `IntoIterator<Item = String>`
// - `IntoIterator<Item = Box<str>>`
// - `IntoIterator<Item = Cow<str>>`
let names: &'static [&str] = 'names: {
// PERF: Reuse items allocation as-is.
// PERF: Reuse arguments strings slice.
if let Some(args) = args_strings {
break 'names args;
}

// PERF: Reuse our args slice allocation.
if let Some(args) = args.cast_ref::<&[&str]>() {
break 'names args;
}

Box::leak(
args.iter()
.map(|arg| -> &str {
// PERF: Use strings as-is.
// PERF: Reuse strings as-is.
if let Some(arg) = arg.cast_ref::<String>() {
return arg;
}
Expand All @@ -91,6 +122,8 @@ impl BenchArgs {
return arg;
}

// Default to `arg_to_string`, which will format via
// either `ToString` or `Debug`.
Box::leak(arg_to_string(arg).into_boxed_str())
})
.collect(),
Expand Down Expand Up @@ -202,22 +235,50 @@ mod tests {
}
}

/// Tests that `&[&str]` reuses the original slice for names.
#[test]
fn str_slice() {
static ARGS: BenchArgs = BenchArgs::new();
static ORIG_ARGS: &[&str] = &["a", "b"];

let runner = ARGS.runner(|| ORIG_ARGS, ToString::to_string, |_, _| {});

let typed_args: Vec<&str> =
runner.args.typed_args::<&&str>().unwrap().iter().copied().copied().collect();
let names = runner.arg_names();

// Test values.
assert_eq!(names, ORIG_ARGS);
assert_eq!(names, typed_args);

// Test addresses.
assert_eq!(names.as_ptr(), ORIG_ARGS.as_ptr());
assert_ne!(names.as_ptr(), typed_args.as_ptr());
}

/// Tests optimizing `IntoIterator<Item = &str>` to reuse the same
/// allocation for also storing argument names.
#[test]
fn str() {
fn str_array() {
static ARGS: BenchArgs = BenchArgs::new();

let runner = ARGS.runner(|| ["a", "b"], ToString::to_string, |_, _| {});

let typed_args = runner.args.typed_args::<&str>().unwrap();
let names = runner.arg_names();

// Test values.
assert_eq!(names, ["a", "b"]);
assert_eq!(names, typed_args);

// Test addresses.
assert_eq!(names.as_ptr(), typed_args.as_ptr());
}

/// Tests optimizing `IntoIterator<Item = String>` to reuse the same
/// allocation for also storing argument names.
#[test]
fn string() {
fn string_array() {
static ARGS: BenchArgs = BenchArgs::new();

let runner =
Expand All @@ -230,8 +291,10 @@ mod tests {
test_eq_ptr(names, typed_args);
}

/// Tests optimizing `IntoIterator<Item = Box<str>>` to reuse the same
/// allocation for also storing argument names.
#[test]
fn box_str() {
fn box_str_array() {
static ARGS: BenchArgs = BenchArgs::new();

let runner = ARGS.runner(
Expand All @@ -247,8 +310,10 @@ mod tests {
test_eq_ptr(names, typed_args);
}

/// Tests optimizing `IntoIterator<Item = Cow<str>>` to reuse the same
/// allocation for also storing argument names.
#[test]
fn cow_str() {
fn cow_str_array() {
static ARGS: BenchArgs = BenchArgs::new();

let runner = ARGS.runner(
Expand Down

0 comments on commit a2bcced

Please sign in to comment.