From 737f533f7e6fbd0e22e18cccfec6cfe194f857d7 Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Thu, 9 Feb 2023 16:36:24 -0700 Subject: [PATCH] perf: add benchmark, optimize Realized about a 10x speedup. - add benches/million-updates.rs - optimization: use rand instead of nanorand (because of thread_rng() initialization being muchf aster) - update README Signed-off-by: Stephen Waits --- .gitignore | 1 + Cargo.toml | 13 ++++++- README.md | 17 ++++++--- benches/million-updates.rs | 52 ++++++++++++++++++++++++++ examples/planetlab.rs | 8 ++-- proptest-regressions/height_vector.txt | 1 + src/height_vector.rs | 14 ++++--- 7 files changed, 90 insertions(+), 16 deletions(-) create mode 100644 benches/million-updates.rs diff --git a/.gitignore b/.gitignore index 96ef6c0..09766cd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ +/flamegraph.svg /target Cargo.lock diff --git a/Cargo.toml b/Cargo.toml index 4ff5bab..d4882a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,13 +20,24 @@ default = ["f32"] f32 = [] f64 = [] +[profile.bench] +debug = true + +[profile.release] +debug = true + [dev-dependencies] assert_approx_eq = "1.1.0" +criterion = "0.4.0" proptest = "1.0.0" serde_json = "1.0.91" +[[bench]] +name = "million-updates" +harness = false + [dependencies] -nanorand = {version = "0.7.0", features = ["wyrand", "getrandom"]} num-traits = "0.2.15" +rand = "0.8.5" serde = {version = "1.0.152", features = ["serde_derive"]} serde_with = "2.2.0" diff --git a/README.md b/README.md index dce10f8..d0c5dc7 100644 --- a/README.md +++ b/README.md @@ -172,10 +172,11 @@ One design goal of this crate is to minimize dependencies. When dependencies are required, I try to be very selective about them (re: bloat and licensing). This crate depends on: -- [`nanorand`](https://crates.io/crates/nanorand): A fast PRNG based on WyRand. - It pulls in `getrandom` as a portable source of entropy. I chose this over - the more commonly used, but heavyweight `rand` because it's significantly - smaller and does just what I need, and not much more. +- [`rand`](https://crates.io/crates/rand): I started with `nanorand`; however, + this is faster because of `rand::thread_rng()`; whereas with `nanorand` we + ended up needing to reimplement that same functionality through something + like `lazy_static!` plus `Mutex`, which is not worth it for a few percentage + points of performance. - [`num-traits`](https://crates.io/crates/num-traits): A brilliant crate which makes it easier to operate on numbers in generics; like using `Float` as a constraint on a generic type. Its convenience outweighs its cost. @@ -190,7 +191,8 @@ This crate depends on: Several crates implemented Vivaldi NC before this one. So, why another? -I had several design goals which the existing crates didn't satisfy: +I had several design goals which the existing crates didn't satisfy, in order +of priority: 1. Provide the *simplest interface possible*. I just want to have some sort of Coordinate struct and be able to update it, and then use it to estimate @@ -202,7 +204,10 @@ I had several design goals which the existing crates didn't satisfy: 3. Serializable/Deserializable by default. These things are useful for sending across networks. The intent is to be able to do that without a bunch of rigamarole. Support `serde` traits by default. -4. Well documented. Well tested. This varies by the crates. +4. Well documented. Well tested. This varies in the existing crates. +5. Performance. While it's a low priority, it should be reasonable. The library + currently does about 10,000 `NetworkCoordinate::update()` calls per + millisecond on my Macbook Pro (Intel i7-9750H (12) @ 2.60GHz). ### Other Vivaldi NC Implementations diff --git a/benches/million-updates.rs b/benches/million-updates.rs new file mode 100644 index 0000000..887be96 --- /dev/null +++ b/benches/million-updates.rs @@ -0,0 +1,52 @@ +use std::time::Duration; + +use criterion::{criterion_group, criterion_main, Criterion}; +use vivaldi_nc::{NetworkCoordinate2D, NetworkCoordinate3D}; + +const NUM_NODES: usize = 1_000; + +fn million_vivaldi_2d_updates() { + // create NUM_NODES NCs + let mut nc: Vec = + (0..NUM_NODES).map(|_| NetworkCoordinate2D::new()).collect(); + + // loop, update each item for every other item + for i in 0..NUM_NODES { + let nc_i = nc[i].clone(); + for (j, nc_j) in nc.iter_mut().enumerate().take(NUM_NODES) { + if i == j { + continue; + } + let rtt = if i > j { i - j } else { j - i }; + // let nc_j = nc[j].clone(); + nc_j.update(&nc_i, Duration::from_millis(rtt.try_into().unwrap())); + } + } +} + +fn million_vivaldi_3d_updates() { + // create NUM_NODES NCs + let mut nc: Vec = + (0..NUM_NODES).map(|_| NetworkCoordinate3D::new()).collect(); + + // loop, update each item for every other item + for i in 0..NUM_NODES { + let nc_i = nc[i].clone(); + for (j, nc_j) in nc.iter_mut().enumerate().take(NUM_NODES) { + if i == j { + continue; + } + let rtt = if i > j { i - j } else { j - i }; + // let nc_j = nc[j].clone(); + nc_j.update(&nc_i, Duration::from_millis(rtt.try_into().unwrap())); + } + } +} + +fn run_benchmarks(c: &mut Criterion) { + c.bench_function("million 2D updates", |b| b.iter(million_vivaldi_2d_updates)); + c.bench_function("million 3D updates", |b| b.iter(million_vivaldi_3d_updates)); +} + +criterion_group!(benches, run_benchmarks); +criterion_main!(benches); diff --git a/examples/planetlab.rs b/examples/planetlab.rs index 5be3340..48c6307 100644 --- a/examples/planetlab.rs +++ b/examples/planetlab.rs @@ -8,7 +8,7 @@ use std::{ time::Duration, }; -use nanorand::{Rng, WyRand}; +use rand::Rng; use vivaldi_nc::NetworkCoordinate; // this will return a Vec> @@ -66,10 +66,10 @@ fn main() { // 4. adjust local NC let mut error = 0.0; for _ in 0..30_000 { - let mut rng = WyRand::new(); + let mut rng = rand::thread_rng(); // 1. choose a local and remote NC - let i_local = rng.generate::() % NUM_NODES; - let i_remote = rng.generate::() % NUM_NODES; + let i_local = rng.gen_range(0..NUM_NODES); + let i_remote = rng.gen_range(0..NUM_NODES); if i_local == i_remote { continue; } diff --git a/proptest-regressions/height_vector.txt b/proptest-regressions/height_vector.txt index 8abd7a4..9ba3387 100644 --- a/proptest-regressions/height_vector.txt +++ b/proptest-regressions/height_vector.txt @@ -9,3 +9,4 @@ cc 38ed1e2c313317809f5469d856d2c1c61e4851c64a156cd1bdd0757fa7c77128 # shrinks to cc b7d1b5e9fe3e33066b583c4532865373179972e9b5786ecc62438f23bc939e87 # shrinks to x = -0.0, y = 0.0, h = -3.448118e-33, m = -0.0 cc 3c4fc4f21c82ffe5bcbc740ca9ab598e0894226acb86a3046a3a24cbbcdbf8e6 # shrinks to x = 0.0, y = 0.0, h = -0.0 cc d1a551d7eb92bf69e3d0cd0b677baac5aaa5a95d6a016fccf67cf57af71c8b0c # shrinks to x = -5.098216e26, y = 0.0, h = 0.0 +cc 7fa6f2f56f2398bae6804c5f0c0f3f47a7304e505c34f3addb676546cefd97f2 # shrinks to x = -0.0, y = 1.500389e38, h = 2.828499e38 diff --git a/src/height_vector.rs b/src/height_vector.rs index bfdc434..d1a4cad 100644 --- a/src/height_vector.rs +++ b/src/height_vector.rs @@ -44,7 +44,7 @@ use core::ops::{Add, Mul, Sub}; -use nanorand::{Rng, WyRand}; +use rand::prelude::*; use serde::{Deserialize, Serialize}; use crate::vector::{self, Vector}; @@ -99,12 +99,12 @@ impl HeightVector { /// A new height vector is a random unit vector pub(crate) fn random() -> Self { - let mut rng = WyRand::new(); + let mut rng = rand::thread_rng(); let mut vec = [0.0; N]; for i in vec.iter_mut().take(N) { - *i = rng.generate::(); + *i = rng.gen::() - 0.5; } - let height = rng.generate::().abs(); + let height = rng.gen::().abs(); Self { position: Vector::::from(vec), height, @@ -255,7 +255,11 @@ mod tests { assert_approx_eq!(a.len(), 1.0); } else { // `HeightVector` we created from proptest values should be valid and have a length - assert_approx_eq!(a.len(), len); + if len.is_nan() || len.is_infinite() { + assert!(a.len().is_nan() || a.len().is_infinite()); + } else { + assert_approx_eq!(a.len(), len); + } } }