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 <steve@waits.net>
This commit is contained in:
swaits committed 2023-02-09 16:36:24 -07:00
1 parent 84a818b9b9
commit 737f533f7e
7 files changed
+90 -16

No files matched your search

+1
View File
@@ -1,2 +1,3 @@
/flamegraph.svg
/target /target
Cargo.lock Cargo.lock
+12 -1
View File
@@ -20,13 +20,24 @@ default = ["f32"]
f32 = [] f32 = []
f64 = [] f64 = []
[profile.bench]
debug = true
[profile.release]
debug = true
[dev-dependencies] [dev-dependencies]
assert_approx_eq = "1.1.0" assert_approx_eq = "1.1.0"
criterion = "0.4.0"
proptest = "1.0.0" proptest = "1.0.0"
serde_json = "1.0.91" serde_json = "1.0.91"
[[bench]]
name = "million-updates"
harness = false
[dependencies] [dependencies]
nanorand = {version = "0.7.0", features = ["wyrand", "getrandom"]}
num-traits = "0.2.15" num-traits = "0.2.15"
rand = "0.8.5"
serde = {version = "1.0.152", features = ["serde_derive"]} serde = {version = "1.0.152", features = ["serde_derive"]}
serde_with = "2.2.0" serde_with = "2.2.0"
+11 -6
View File
@@ -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). are required, I try to be very selective about them (re: bloat and licensing).
This crate depends on: This crate depends on:
- [`nanorand`](https://crates.io/crates/nanorand): A fast PRNG based on WyRand. - [`rand`](https://crates.io/crates/rand): I started with `nanorand`; however,
It pulls in `getrandom` as a portable source of entropy. I chose this over this is faster because of `rand::thread_rng()`; whereas with `nanorand` we
the more commonly used, but heavyweight `rand` because it's significantly ended up needing to reimplement that same functionality through something
smaller and does just what I need, and not much more. 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 - [`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 makes it easier to operate on numbers in generics; like using `Float` as a
constraint on a generic type. Its convenience outweighs its cost. 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? 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 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 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 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 across networks. The intent is to be able to do that without a bunch of
rigamarole. Support `serde` traits by default. 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 ### Other Vivaldi NC Implementations
+52
View File
@@ -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<NetworkCoordinate2D> =
(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<NetworkCoordinate3D> =
(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);
+4 -4
View File
@@ -8,7 +8,7 @@ use std::{
time::Duration, time::Duration,
}; };
use nanorand::{Rng, WyRand}; use rand::Rng;
use vivaldi_nc::NetworkCoordinate; use vivaldi_nc::NetworkCoordinate;
// this will return a Vec<Vec<Duration>> // this will return a Vec<Vec<Duration>>
@@ -66,10 +66,10 @@ fn main() {
// 4. adjust local NC // 4. adjust local NC
let mut error = 0.0; let mut error = 0.0;
for _ in 0..30_000 { for _ in 0..30_000 {
let mut rng = WyRand::new(); let mut rng = rand::thread_rng();
// 1. choose a local and remote NC // 1. choose a local and remote NC
let i_local = rng.generate::<usize>() % NUM_NODES; let i_local = rng.gen_range(0..NUM_NODES);
let i_remote = rng.generate::<usize>() % NUM_NODES; let i_remote = rng.gen_range(0..NUM_NODES);
if i_local == i_remote { if i_local == i_remote {
continue; continue;
} }
+1
View File
@@ -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 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 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 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
+9 -5
View File
@@ -44,7 +44,7 @@
use core::ops::{Add, Mul, Sub}; use core::ops::{Add, Mul, Sub};
use nanorand::{Rng, WyRand}; use rand::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::vector::{self, Vector}; use crate::vector::{self, Vector};
@@ -99,12 +99,12 @@ impl<const N: usize> HeightVector<N> {
/// A new height vector is a random unit vector /// A new height vector is a random unit vector
pub(crate) fn random() -> Self { pub(crate) fn random() -> Self {
let mut rng = WyRand::new(); let mut rng = rand::thread_rng();
let mut vec = [0.0; N]; let mut vec = [0.0; N];
for i in vec.iter_mut().take(N) { for i in vec.iter_mut().take(N) {
*i = rng.generate::<FloatType>(); *i = rng.gen::<FloatType>() - 0.5;
} }
let height = rng.generate::<FloatType>().abs(); let height = rng.gen::<FloatType>().abs();
Self { Self {
position: Vector::<FloatType, N>::from(vec), position: Vector::<FloatType, N>::from(vec),
height, height,
@@ -255,7 +255,11 @@ mod tests {
assert_approx_eq!(a.len(), 1.0); assert_approx_eq!(a.len(), 1.0);
} else { } else {
// `HeightVector` we created from proptest values should be valid and have a length // `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);
}
} }
} }