Slide 1

Slide 1 text

Algorithms in Rust Motivations, Tips and Tools By Tristan Hume

Slide 2

Slide 2 text

Intro: What this talk is about — "Algorithms": Tricky code for computation — Found in most programs to varying extents — Talk focuses on Rust and Rust accessories

Slide 3

Slide 3 text

Intro: Overview — Intro — What makes Rust good for algorithms — Tips: Writing and testing algorithms in Rust — Tools: Lots of handy Rust crates

Slide 4

Slide 4 text

Intro: Personal examples — xi-editor: CRDT text synchronization — syntect: Highlighting and parsing procedures — seqalign_pathing: Dynamic programming with A* pathfinding — Rate With Science: Wikipedia link graph search — Dayder: Fast correlation calculations on lots of data

Slide 5

Slide 5 text

Intro: Notes — A lot of this applies to other languages — Just not all in the same language, except Rust — http://thume.ca/talks has a link to the slides — Code examples are real code from projects I've worked on — Only adulterated by deleting unimportant lines — Except crate examples, which are from crate docs

Slide 6

Slide 6 text

Why Rust for algorithms?

Slide 7

Slide 7 text

Easy testing #[cfg(test)] mod tests { use super::*; #[test] fn global_repo() { use std::str::FromStr; assert_eq!(Scope::new("source.php").unwrap(), Scope::new("source.php").unwrap()); assert!(Scope::from_str("1.2.3.4").is_ok()); } }

Slide 8

Slide 8 text

Easy testing: Doc Tests /// Tests if this scope is a prefix of another scope. /// Note that the empty scope is always a prefix. /// /// ``` /// use syntect::parsing::Scope; /// assert!( Scope::new("string").unwrap() /// .is_prefix_of(Scope::new("string.quoted").unwrap())); /// assert!( Scope::new("string.quoted").unwrap() /// .is_prefix_of(Scope::new("string.quoted").unwrap())); /// ``` pub fn is_prefix_of(self, s: Scope) -> bool { // ... }

Slide 9

Slide 9 text

Safe Refactoring — Strong type system: I feel safe that I won't break everything — Great compiler errors: I know what I need to change — Great index of these: https://doc.rust-lang.org/error-index.html — Easy testing: Easy to write tests before you refactor

Slide 10

Slide 10 text

Fearless Concurrency use rayon::prelude::*; let mut correlations : Vec = Vec::new(); possibilities.par_iter().map(|poss| { if let Some((xs, ys)) = pairinate(&query_series, poss) { pearson_correlation_coefficient(&xs, &ys) as f32 } else { 0.0 } }).collect_into(&mut correlations);

Slide 11

Slide 11 text

Pattern Matching / Sum Types match *op { ScopeStackOp::Push(scope) => { self.scopes.push(scope); hook(BasicScopeStackOp::Push(scope), self.as_slice()); } ScopeStackOp::Pop(count) => { for _ in 0..count { self.scopes.pop(); hook(BasicScopeStackOp::Pop, self.as_slice()); } } ScopeStackOp::Clear(amount) => { ... } ScopeStackOp::Restore => { ... } ScopeStackOp::Noop => (), }

Slide 12

Slide 12 text

Auto-Derive #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] pub struct ScopeStack { clear_stack: Vec>, scopes: Vec, } impl fmt::Debug for Scope { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let s = self.build_string(); write!(f, "<{}>", s) } }

Slide 13

Slide 13 text

Print Debugging #[derive(Debug)] struct TheOnlyFakeExample { fake: usize, example: Vec, } fn main() { let ex = TheOnlyFakeExample { fake: 5, example: vec![String::from("test\t\n")] }; println!("{:?}",ex); // debug print println!("{:#?}",ex); // pretty print }

Slide 14

Slide 14 text

Print Debugging: Output TheOnlyFakeExample { fake: 5, example: ["test\t\n"] } TheOnlyFakeExample { fake: 5, example: [ "test\t\n" ] }

Slide 15

Slide 15 text

Tips/Advice

Slide 16

Slide 16 text

Understanding Existing Code — Refactor and document! — I did this when learning Xi's codebase, it helped a lot — Confusing naming? Rename it! — Confusing code turns out to be wrong or suboptimal? Fix it! — Spend lots of time understanding something? Add a comment! — Ideally get code review from the original author — You help yourself and everyone who comes after you!

Slide 17

Slide 17 text

Rust's can build powerful primitives — Isolate complex logic — Rust's type system allows you to create nice abstractions — LLVM's optimizer means these abstractions usually come at no performance cost

Slide 18

Slide 18 text

Powerful Primitives: Before pub fn union(&self, other: &Subset) -> Subset { let mut sb = SubsetBuilder::new(); let mut i = 0; let mut j = 0; loop { let (next_beg, mut next_end) = if i == self.0.len() { if j == other.0.len() { break; } else { let del = other.0[j]; j += 1; del } } else if j == other.0.len() || self.0[i].0 < other.0[j].0 { let del = self.0[i]; i += 1; del } else { let del = other.0[j]; j += 1; del }; loop { if i < self.0.len() && self.0[i].0 <= next_end { next_end = max(next_end, self.0[i].1); i += 1; continue; } else if j < other.0.len() && other.0[j].0 <= next_end { next_end = max(next_end, other.0[j].1); j += 1; continue; } else { break; } } sb.add_range(next_beg, next_end); } sb.build() } pub fn transform_shrink(&self, other: &Subset) -> Subset { let mut sb = SubsetBuilder::new(); let mut last = 0; let mut i = 0; let mut y = 0; for &(b, e) in &self.0 { if i < other.0.len() && other.0[i].0 < last && other.0[i].1 < b { sb.add_range(y, other.0[i].1 + y - last); i += 1; } while i < other.0.len() && other.0[i].1 < b { sb.add_range(other.0[i].0 + y - last, other.0[i].1 + y - last); i += 1; } if i < other.0.len() && other.0[i].0 < b { sb.add_range(max(last, other.0[i].0) + y - last, b + y - last); } while i < other.0.len() && other.0[i].1 < e { i += 1; } y += b - last; last = e; } if i < other.0.len() && other.0[i].0 < last { sb.add_range(y, other.0[i].1 + y - last); i += 1; } for &(b, e) in &other.0[i..] { sb.add_range(b + y - last, e + y - last); } sb.build() }

Slide 19

Slide 19 text

Powerful Primitives: After pub fn union(&self, other: &Subset) -> Subset { let mut sb = SubsetBuilder::new(); for zseg in self.zip(other) { sb.push_segment(zseg.len, zseg.a_count + zseg.b_count); } sb.build() } pub fn transform_shrink(&self, other: &Subset) -> Subset { let mut sb = SubsetBuilder::new(); for zseg in other.zip(self) { if zseg.b_count == 0 { sb.push_segment(zseg.len, zseg.a_count); } } sb.build() }

Slide 20

Slide 20 text

Powerful Primitives: The Abstraction pub fn zip<'a>(&'a self, other: &'a Subset) -> ZipIter<'a> { ZipIter { a_segs: self.segments.as_slice(), b_segs: other.segments.as_slice(), a_i: 0, b_i: 0, a_consumed: 0, b_consumed: 0, consumed: 0, } } /// See `Subset::zip` pub struct ZipIter<'a> { a_segs: &'a [Segment], b_segs: &'a [Segment], a_i: usize, b_i: usize, a_consumed: usize, b_consumed: usize, pub consumed: usize, } /// See `Subset::zip` #[derive(Clone, Debug)] pub struct ZipSegment { len: usize, a_count: usize, b_count: usize, } impl<'a> Iterator for ZipIter<'a> { type Item = ZipSegment; /// Consume as far as possible from `self.consumed` until reaching a /// segment boundary in either `Subset`, and return the resulting /// `ZipSegment`. Will panic if it reaches the end of one `Subset` before /// the other, that is when they have different total length. fn next(&mut self) -> Option { match (self.a_segs.get(self.a_i), self.b_segs.get(self.b_i)) { (None, None) => None, (None, Some(_)) | (Some(_), None) => panic!("can't zip Subsets of different base lengths."), (Some(&Segment {len: a_len, count: a_count}), Some(&Segment {len: b_len, count: b_count})) => { let len = if a_len + self.a_consumed == b_len + self.b_consumed { self.a_consumed += a_len; self.a_i += 1; self.b_consumed += b_len; self.b_i += 1; self.a_consumed - self.consumed } else if a_len + self.a_consumed < b_len + self.b_consumed { self.a_consumed += a_len; self.a_i += 1; self.a_consumed - self.consumed } else { self.b_consumed += b_len; self.b_i += 1; self.b_consumed - self.consumed }; self.consumed += len; Some(ZipSegment {len, a_count, b_count}) } } } }

Slide 21

Slide 21 text

Use iterator methods to avoid complex loop structures pub fn get_style(&self, path: &[Scope]) -> StyleModifier { let max_item = self.theme .scopes .iter() .filter_map(|item| { item.scope .does_match(path) .map(|score| (score, item)) }) .max_by_key(|&(score, _)| score) .map(|(_, item)| item); StyleModifier { foreground: max_item.and_then(|item| item.style.foreground), background: max_item.and_then(|item| item.style.background), font_style: max_item.and_then(|item| item.style.font_style), } }

Slide 22

Slide 22 text

Default to Vec arenas for cyclical data structures — Where your references might form cycles, instead use indices into a Vec that stores all your nodes — Requires passing around a reference to the arena as a parameter ! — Solves mutability, borrow checking and memory management issues — Gives you good node allocation speed and locality as a side effect! "

Slide 23

Slide 23 text

Vec arenas for cyclical structures: better than the alternatives — Using Rc/Weak/RefCell can be a pain to deal with — I used Rc/Weak/RefCell in syntect and regret it — It lead to API gotchas that could cause panics — because of Weaks allowing you to drop necessary state — An arena would have made these mistakes compile errors — It made some things nigh-impossible to parallelize — Made some iterators slow that should have been fast — Accessing things required a bunch of boilerplate

Slide 24

Slide 24 text

Tools / Crates

Slide 25

Slide 25 text

Property Testing #[macro_use] extern crate quickcheck; quickcheck! { fn prop(xs: Vec) -> bool { xs == reverse(&reverse(&xs)) } } #[macro_use] extern crate proptest; proptest! { #[test] fn parses_all_valid_dates(ref s in "[0-9]{4}-[0-9]{2}-[0-9]{2}") { parse_date(s).unwrap(); } }

Slide 26

Slide 26 text

Fuzzing — Like quickcheck, but uses instrumentation to find interesting tests — Fastest at byte buffers, but also supports all types quickcheck does — Three crates for different fuzzers: honggfuzz, cargo-fuzz, afl — Doesn't integrate with cargo test like property testing libraries

Slide 27

Slide 27 text

Fuzzing — My impression: honggfuzz-rs is the easiest and best fuzzing crate #[macro_use] extern crate honggfuzz; fn main() { loop { fuzz!(|data: &[u8]| { if data.len() != 3 {return} if data[0] != b'q' {return} if data[1] != b'w' {return} if data[2] != b'e' {return} panic!("BOOM") }); } }

Slide 28

Slide 28 text

Benchmarking cargo bench works on nightly Rust, or with the bencher crate #[bench] fn bench_stack_matching(b: &mut Bencher) { let s = "source.js meta.group.js meta.group.js"; let stack = ScopeStack::from_str(s).unwrap(); let selector = ScopeStack::from_str("source meta.group.js").unwrap(); b.iter(|| { let res = selector.does_match(stack.as_slice()); test::black_box(res); }); }

Slide 29

Slide 29 text

Benchmarking cargo-benchcmp crate gives you nice comparisons $ cargo benchcmp module1:: module2:: benchmark-output name dense_boxed:: ns/iter dense:: ns/iter diff ns/iter diff % speedup ac_one_byte 354 (28248 MB/s) 349 (28653 MB/s) -5 -1.41% x 1.01 ac_one_prefix_byte_every_match 150,581 (66 MB/s) 112,957 (88 MB/s) -37,624 -24.99% x 1.33 ac_one_prefix_byte_no_match 354 (28248 MB/s) 350 (28571 MB/s) -4 -1.13% x 1.01

Slide 30

Slide 30 text

Benchmarking criterion can do significance testing to tell you if performance improved, and generate fancy reports.

Slide 31

Slide 31 text

Parallelizing — rayon has easy-to use threadpools and parallel iterators — crossbeam includes fast lock-free channels, queues, stacks and more — Includes scoped threads that can access stack data — Also includes a library for GC in lock-free data structures

Slide 32

Slide 32 text

SIMD — Rust nightly includes access to SIMD intrinsics — faster provides easy SIMD iterators use faster::*; let lots_of_3s = (&[-123.456f32; 128][..]).simd_iter() .simd_map(f32s(0.0), |v| { f32s(9.0) * v.abs().sqrt().rsqrt().ceil().sqrt() - f32s(4.0) - f32s(2.0) }) .scalar_collect();

Slide 33

Slide 33 text

Geometry — cgmath: Vector, Matrix, Point, a few others — nalgebra: Nice docs, more helpers, more types (e.g Translation) — euclid: Maximum type safety (on the other hand, maximum hassle) use euclid::*; pub struct ScreenSpace; pub type ScreenPoint = TypedPoint2D; pub type ScreenSize = TypedSize2D; pub struct WorldSpace; pub type WorldPoint = TypedPoint3D; pub type ProjectionMatrix = TypedTransform3D;

Slide 34

Slide 34 text

Cryptographic hashes — RustCrypto/hashes provides many crypto-hash crates — Cryptographic hashes for are great for many non-crypto purposes — distributed coordination, caches, Merkle trees, etc... use blake2::{Blake2b, Digest}; let mut hasher = Blake2b::new(); hasher.input(b"Hello world!"); hasher.input("String data".as_bytes()); // Note that calling `result()` consumes hasher let hash = hasher.result(); println!("Result: {:x}", hash);

Slide 35

Slide 35 text

Faster hash maps — fxhash faster hash function for HashMap and HashSet — Rust's default hash function is collision attack resistant — fxhash gives that up for speed and can hash 8 bytes at a time — Used in rustc and Servo for a noticeable speedup — indexmap: Hash map with fast iteration, maintains inertion order

Slide 36

Slide 36 text

Handy data structures — slice_deque: sliceable queue using memory mapping — slab: for re-using lots of objects of the same size — uluru: LRU cache — priority-queue: a priority queue

Slide 37

Slide 37 text

Misc — rust-pretty-assertions: makes assert_eq show a diff on failure — pest: nice parser generator — serde: Automatic serialization/deserialization — Supports many formats and arbitrary data structures — bincode: binary serialization format for serde — good for caches, storing large data structures — itertools: extra iterator functions! — differential-dataflow: some kind of magic incremental parallel distributed dataflow madness, if you can understand it

Slide 38

Slide 38 text

The End Go forth and write code! (And test, benchmark and optimize it) http://thume.ca/talks