├── .gitignore ├── Cargo.toml ├── README.md ├── benches ├── bench_dyntrait.rs ├── bench_innercheck.rs ├── bench_linkedlist.rs └── bench_soa.rs └── src ├── dyntrait.rs ├── lib.rs ├── linked_list.rs ├── pattern_match.rs └── soa.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "data_oriented_example" 3 | version = "0.1.0" 4 | authors = ["James Roger McMurray "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | rand = "0.7" 11 | criterion = "0.3" 12 | 13 | [[bench]] 14 | name = "bench_soa" 15 | harness = false 16 | 17 | [[bench]] 18 | name = "bench_innercheck" 19 | harness = false 20 | 21 | [[bench]] 22 | name = "bench_linkedlist" 23 | harness = false 24 | 25 | [[bench]] 26 | name = "bench_dyntrait" 27 | harness = false 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # data-oriented-example 2 | 3 | Experimenting with [Data Oriented Design](https://en.wikipedia.org/wiki/Data-oriented_design) in Rust. 4 | 5 | Written for this blog post: http://jamesmcm.github.io/blog/2020/07/25/intro-dod/#en 6 | 7 | Done: 8 | 9 | * Benchmark of [Array of Structs vs. Struct of Arrays](https://en.wikipedia.org/wiki/AoS_and_SoA) 10 | 11 | SoA gets unrolled by compiler, results in ~50% speed up. 12 | Godbolt: https://godbolt.org/z/d8bjMb 13 | 14 | * Benchmark of pattern matching inside hot loop vs. outside 15 | 16 | Outside results in 50% speed up. 17 | 18 | * Benchmark of Linked List iteration vs. contiguous Vector 19 | 20 | Vector shows consistent 90% speed up vs. Linked List iteration 21 | 22 | * Benchmark of dynamic dispatch vs. monomorphisation 23 | 24 | Dynamic dispatch impedes vectorisation due to indirection, otherwise 25 | cost of lookup in vtable is small 26 | -------------------------------------------------------------------------------- /benches/bench_dyntrait.rs: -------------------------------------------------------------------------------- 1 | use core::time::Duration; 2 | use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; 3 | use data_oriented_example::{ 4 | gen_vec_dyn, gen_vecs, gen_vecs_box, run_dyn, run_dyn_square, run_vecs, run_vecs_box, 5 | run_vecs_box_square, run_vecs_square, MyTrait, 6 | }; 7 | 8 | fn bench_dyntrait(c: &mut Criterion) { 9 | let mut group = c.benchmark_group("DynTrait"); 10 | group.warm_up_time(Duration::from_millis(1000)); 11 | group.measurement_time(Duration::from_millis(15000)); 12 | group.sample_size(100); 13 | 14 | for i in [ 15 | 100, 1000, 2000, 5000, 10000, 50000, 100000, 1000000, 3000000, 5000000, 16 | ] 17 | .iter() 18 | { 19 | let mut vec = gen_vecs(*i); 20 | let mut vec_box = gen_vecs_box(*i); 21 | let mut vec_dyn: Vec> = gen_vec_dyn(*i); 22 | 23 | // group.bench_with_input(BenchmarkId::new("Vec", i), i, |b, _i| { 24 | // b.iter(|| { 25 | // run_vecs(&vec.0); 26 | // run_vecs(&vec.1); 27 | // }) 28 | // }); 29 | // group.bench_with_input(BenchmarkId::new("Vec>", i), i, |b, _i| { 30 | // b.iter(|| { 31 | // run_vecs_box(&vec_box.0); 32 | // run_vecs_box(&vec_box.1); 33 | // }) 34 | // }); 35 | // group.bench_with_input(BenchmarkId::new("Vec", i), i, |b, _i| { 36 | // b.iter(|| { 37 | // run_dyn(&vec_dyn); 38 | // }) 39 | // }); 40 | 41 | group.bench_with_input(BenchmarkId::new("Vec Square", i), i, |b, _i| { 42 | b.iter(|| { 43 | run_vecs_square(&mut vec.0); 44 | run_vecs_square(&mut vec.1); 45 | }) 46 | }); 47 | group.bench_with_input(BenchmarkId::new("Vec> Square", i), i, |b, _i| { 48 | b.iter(|| { 49 | run_vecs_box_square(&mut vec_box.0); 50 | run_vecs_box_square(&mut vec_box.1); 51 | }) 52 | }); 53 | group.bench_with_input(BenchmarkId::new("Vec Square", i), i, |b, _i| { 54 | b.iter(|| { 55 | run_dyn_square(&mut vec_dyn); 56 | }) 57 | }); 58 | } 59 | group.finish(); 60 | } 61 | 62 | criterion_group!(benches, bench_dyntrait); 63 | criterion_main!(benches); 64 | -------------------------------------------------------------------------------- /benches/bench_innercheck.rs: -------------------------------------------------------------------------------- 1 | use core::time::Duration; 2 | use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; 3 | use data_oriented_example::{gen_mixed, gen_separate, run_mixed, run_separate}; 4 | 5 | fn bench_innercheck(c: &mut Criterion) { 6 | let mut group = c.benchmark_group("InnerCheck"); 7 | group.warm_up_time(Duration::from_millis(1000)); 8 | group.measurement_time(Duration::from_millis(15000)); 9 | group.sample_size(100); 10 | 11 | for i in [ 12 | 100, 1000, 2000, 5000, 10000, 50000, 100000, 1000000, 3000000, 5000000, 13 | ] 14 | .iter() 15 | { 16 | let mixed = gen_mixed(*i); 17 | let separate = gen_separate(*i); 18 | 19 | group.bench_with_input(BenchmarkId::new("Mixed", i), i, |b, _i| { 20 | b.iter(|| run_mixed(&mixed)) 21 | }); 22 | group.bench_with_input(BenchmarkId::new("Separate", i), i, |b, _i| { 23 | b.iter(|| run_separate(&separate.0, &separate.1, &separate.2)) 24 | }); 25 | } 26 | group.finish(); 27 | } 28 | 29 | criterion_group!(benches, bench_innercheck); 30 | criterion_main!(benches); 31 | -------------------------------------------------------------------------------- /benches/bench_linkedlist.rs: -------------------------------------------------------------------------------- 1 | use core::time::Duration; 2 | use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; 3 | use data_oriented_example::{gen_list, gen_vec, run_list, run_vec}; 4 | 5 | fn bench_linkedlist(c: &mut Criterion) { 6 | let mut group = c.benchmark_group("LinkedList"); 7 | group.warm_up_time(Duration::from_millis(1000)); 8 | group.measurement_time(Duration::from_millis(15000)); 9 | group.sample_size(100); 10 | 11 | for i in [ 12 | 1000, 5000, 10000, 20000, 50000, 100000, 500000, 1000000, 3000000, 5000000, 13 | ] 14 | .iter() 15 | { 16 | let mut ll = gen_list(*i); 17 | let mut v = gen_vec(*i); 18 | 19 | group.bench_with_input(BenchmarkId::new("LinkedList", i), i, |b, _i| { 20 | b.iter(|| run_list(&mut ll)) 21 | }); 22 | group.bench_with_input(BenchmarkId::new("Vector", i), i, |b, _i| { 23 | b.iter(|| run_vec(&mut v)) 24 | }); 25 | } 26 | group.finish(); 27 | } 28 | 29 | criterion_group!(benches, bench_linkedlist); 30 | criterion_main!(benches); 31 | -------------------------------------------------------------------------------- /benches/bench_soa.rs: -------------------------------------------------------------------------------- 1 | use core::time::Duration; 2 | use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; 3 | use data_oriented_example::{gen_dop, gen_oop, run_dop, run_oop}; 4 | 5 | fn bench_soa(c: &mut Criterion) { 6 | let mut group = c.benchmark_group("ApplyMotion"); 7 | group.warm_up_time(Duration::from_millis(1000)); 8 | group.measurement_time(Duration::from_millis(15000)); 9 | group.sample_size(100); 10 | 11 | for i in [ 12 | 100, 1000, 2000, 5000, 10000, 50000, 100000, 1000000, 3000000, 5000000, 13 | ] 14 | .iter() 15 | { 16 | let mut oop_input = gen_oop(*i); 17 | let mut dop_input = gen_dop(*i); 18 | 19 | group.bench_with_input(BenchmarkId::new("OOP", i), i, |b, _i| { 20 | b.iter(|| run_oop(&mut oop_input)) 21 | }); 22 | group.bench_with_input(BenchmarkId::new("DOP", i), i, |b, _i| { 23 | b.iter(|| run_dop(&mut dop_input)) 24 | }); 25 | } 26 | group.finish(); 27 | } 28 | 29 | criterion_group!(benches, bench_soa); 30 | criterion_main!(benches); 31 | -------------------------------------------------------------------------------- /src/dyntrait.rs: -------------------------------------------------------------------------------- 1 | use rand::Rng; 2 | 3 | pub struct Foo { 4 | id: usize, 5 | } 6 | 7 | pub struct Bar { 8 | id: usize, 9 | } 10 | 11 | pub trait MyTrait { 12 | fn test(&self) -> String; 13 | fn square_id(&mut self); 14 | } 15 | 16 | impl MyTrait for Foo { 17 | fn test(&self) -> String { 18 | format!("Foo_{}", self.id) 19 | } 20 | 21 | fn square_id(&mut self) { 22 | self.id = self.id * self.id; 23 | } 24 | } 25 | 26 | impl MyTrait for Bar { 27 | fn test(&self) -> String { 28 | format!("Bar_{}", self.id) 29 | } 30 | 31 | fn square_id(&mut self) { 32 | self.id = self.id * self.id * self.id; 33 | } 34 | } 35 | 36 | pub fn gen_vecs(n: usize) -> (Vec, Vec) { 37 | let mut out = Vec::with_capacity(n); 38 | let mut out2 = Vec::with_capacity(n); 39 | for i in 0..n { 40 | out.push(Foo { id: i }); 41 | out2.push(Bar { id: i }); 42 | } 43 | (out, out2) 44 | } 45 | 46 | pub fn gen_vecs_box(n: usize) -> (Vec>, Vec>) { 47 | let mut out = Vec::with_capacity(n); 48 | let mut out2 = Vec::with_capacity(n); 49 | for i in 0..n { 50 | out.push(Box::new(Foo { id: i })); 51 | out2.push(Box::new(Bar { id: i })); 52 | } 53 | (out, out2) 54 | } 55 | 56 | pub fn gen_vec_dyn(n: usize) -> Vec> { 57 | let mut rng = rand::thread_rng(); 58 | let mut out: Vec> = Vec::with_capacity(n); 59 | for i in 0..(2 * n) { 60 | if rng.gen_bool(0.5) { 61 | out.push(Box::new(Foo { id: i })); 62 | } else { 63 | out.push(Box::new(Bar { id: i })); 64 | } 65 | } 66 | out 67 | } 68 | 69 | pub fn run_vecs(x: &Vec) -> Vec { 70 | x.into_iter().map(|x| x.test()).collect() 71 | } 72 | 73 | pub fn run_vecs_box(x: &Vec>) -> Vec { 74 | x.into_iter().map(|x| x.test()).collect() 75 | } 76 | 77 | pub fn run_dyn(x: &Vec>) -> Vec { 78 | x.into_iter().map(|x| x.test()).collect() 79 | } 80 | 81 | pub fn run_vecs_square(x: &mut Vec) { 82 | x.iter_mut().for_each(|x| x.square_id()) 83 | } 84 | 85 | pub fn run_vecs_box_square(x: &mut Vec>) { 86 | x.iter_mut().for_each(|x| x.square_id()) 87 | } 88 | 89 | pub fn run_dyn_square(x: &mut Vec>) { 90 | x.iter_mut().for_each(|x| x.square_id()) 91 | } 92 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod dyntrait; 2 | mod linked_list; 3 | mod pattern_match; 4 | mod soa; 5 | 6 | pub use dyntrait::{ 7 | gen_vec_dyn, gen_vecs, gen_vecs_box, run_dyn, run_dyn_square, run_vecs, run_vecs_box, 8 | run_vecs_box_square, run_vecs_square, MyTrait, 9 | }; 10 | pub use linked_list::{gen_list, gen_vec, run_list, run_vec}; 11 | pub use pattern_match::{gen_mixed, gen_separate, run_mixed, run_separate}; 12 | pub use soa::{gen_dop, gen_oop, run_dop, run_oop, DOPlayers, Player}; 13 | 14 | fn main() { 15 | println!("Hello, world!"); 16 | } 17 | -------------------------------------------------------------------------------- /src/linked_list.rs: -------------------------------------------------------------------------------- 1 | use rand::Rng; 2 | // LinkedList is doubly-linked 3 | use std::collections::LinkedList; 4 | 5 | pub fn gen_list(n: usize) -> LinkedList { 6 | let mut rng = rand::thread_rng(); 7 | let mut out = LinkedList::new(); 8 | for _i in 0..n { 9 | out.push_back(rng.gen_range(1, 1000)); 10 | } 11 | out 12 | } 13 | 14 | pub fn run_list(list: &mut LinkedList) { 15 | list.iter_mut().for_each(|x| { 16 | *x = *x * *x; 17 | }); 18 | } 19 | 20 | pub fn gen_vec(n: usize) -> Vec { 21 | let mut rng = rand::thread_rng(); 22 | let mut out = Vec::with_capacity(n); 23 | for _i in 0..n { 24 | out.push(rng.gen_range(1, 1000)); 25 | } 26 | out 27 | } 28 | 29 | pub fn run_vec(list: &mut Vec) { 30 | list.iter_mut().for_each(|x| { 31 | *x = *x * *x; 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /src/pattern_match.rs: -------------------------------------------------------------------------------- 1 | use rand::Rng; 2 | 3 | #[derive(Copy, Clone)] 4 | pub struct Foo { 5 | x: i32, 6 | calc_type: CalcType, 7 | } 8 | 9 | #[derive(Copy, Clone)] 10 | pub enum CalcType { 11 | Identity, 12 | Square, 13 | Cube, 14 | } 15 | 16 | pub fn gen_mixed(n: usize) -> Vec { 17 | let mut out = Vec::with_capacity(n); 18 | let mut rng = rand::thread_rng(); 19 | for i in 0..n { 20 | out.push(Foo { 21 | x: rng.gen_range(0, 5000), 22 | calc_type: match rng.gen_range(0, 3) { 23 | 0 => CalcType::Identity, 24 | 1 => CalcType::Square, 25 | 2 => CalcType::Cube, 26 | _ => CalcType::Identity, 27 | }, 28 | }); 29 | } 30 | out 31 | } 32 | 33 | pub fn gen_separate(n: usize) -> (Vec, Vec, Vec) { 34 | let mut x = Vec::with_capacity(n); 35 | let mut y = Vec::with_capacity(n); 36 | let mut z = Vec::with_capacity(n); 37 | let mut rng = rand::thread_rng(); 38 | for i in 0..n { 39 | match rng.gen_range(0, 3) { 40 | 0 => { 41 | x.push(Foo { 42 | x: rng.gen_range(0, 5000), 43 | calc_type: CalcType::Identity, 44 | }); 45 | } 46 | 1 => { 47 | y.push(Foo { 48 | x: rng.gen_range(0, 5000), 49 | calc_type: CalcType::Square, 50 | }); 51 | } 52 | _ => { 53 | z.push(Foo { 54 | x: rng.gen_range(0, 5000), 55 | calc_type: CalcType::Cube, 56 | }); 57 | } 58 | }; 59 | } 60 | (x, y, z) 61 | } 62 | 63 | pub fn run_mixed(x: &[Foo]) -> Vec { 64 | x.into_iter() 65 | .map(|x| match x.calc_type { 66 | CalcType::Identity => x.x, 67 | CalcType::Square => x.x * x.x, 68 | CalcType::Cube => x.x * x.x * x.x, 69 | }) 70 | .collect() 71 | } 72 | 73 | pub fn run_separate(x: &[Foo], y: &[Foo], z: &[Foo]) -> (Vec, Vec, Vec) { 74 | let x = x.into_iter().map(|x| x.x).collect(); 75 | let y = y.into_iter().map(|x| x.x * x.x).collect(); 76 | let z = z.into_iter().map(|x| x.x * x.x * x.x).collect(); 77 | (x, y, z) 78 | } 79 | -------------------------------------------------------------------------------- /src/soa.rs: -------------------------------------------------------------------------------- 1 | use rand::Rng; 2 | 3 | pub struct Player { 4 | name: String, 5 | health: f64, 6 | location: (f64, f64), 7 | velocity: (f64, f64), 8 | acceleration: (f64, f64), 9 | } 10 | 11 | pub struct DOPlayers { 12 | names: Vec, 13 | health: Vec, 14 | locations: Vec<(f64, f64)>, 15 | velocities: Vec<(f64, f64)>, 16 | acceleration: Vec<(f64, f64)>, 17 | } 18 | 19 | pub fn gen_oop(n: usize) -> Vec { 20 | let mut rng = rand::thread_rng(); 21 | let mut players = Vec::with_capacity(n); 22 | for i in 0..n { 23 | players.push(Player { 24 | name: format!("player_name_{}", i), 25 | health: 100.0, 26 | location: (rng.gen_range(0.0, 10.0), rng.gen_range(0.0, 10.0)), 27 | velocity: (rng.gen_range(0.0, 10.0), rng.gen_range(0.0, 10.0)), 28 | acceleration: (rng.gen_range(0.0, 10.0), rng.gen_range(0.0, 10.0)), 29 | }); 30 | } 31 | players 32 | } 33 | 34 | pub fn gen_dop(n: usize) -> DOPlayers { 35 | let mut rng = rand::thread_rng(); 36 | 37 | let mut names = Vec::with_capacity(n); 38 | let mut health = Vec::with_capacity(n); 39 | let mut locations = Vec::with_capacity(n); 40 | let mut velocities = Vec::with_capacity(n); 41 | let mut acceleration = Vec::with_capacity(n); 42 | 43 | for i in 0..n { 44 | names.push(format!("player_name_{}", i)); 45 | health.push(100.0); 46 | locations.push((rng.gen_range(0.0, 10.0), rng.gen_range(0.0, 10.0))); 47 | velocities.push((rng.gen_range(0.0, 10.0), rng.gen_range(0.0, 10.0))); 48 | acceleration.push((rng.gen_range(0.0, 10.0), rng.gen_range(0.0, 10.0))); 49 | } 50 | DOPlayers { 51 | names, 52 | health, 53 | locations, 54 | velocities, 55 | acceleration, 56 | } 57 | } 58 | 59 | pub fn run_oop(players: &mut Vec) { 60 | for player in players.iter_mut() { 61 | player.location = ( 62 | player.location.0 + player.velocity.0, 63 | player.location.1 + player.velocity.1, 64 | ); 65 | player.velocity = ( 66 | player.velocity.0 + player.acceleration.0, 67 | player.velocity.1 + player.acceleration.1, 68 | ); 69 | } 70 | } 71 | 72 | pub fn run_dop(world: &mut DOPlayers) { 73 | for (pos, (vel, acc)) in world 74 | .locations 75 | .iter_mut() 76 | .zip(world.velocities.iter_mut().zip(world.acceleration.iter())) 77 | { 78 | *pos = (pos.0 + vel.0, pos.1 + vel.1); 79 | *vel = (vel.0 + acc.0, vel.1 + acc.1); 80 | } 81 | } 82 | --------------------------------------------------------------------------------