Algorithms in Rust

Algorithms in Rust

Talk given at the Bay Area Rust Meetup

7f9969d3b17c6fd9d604ad2c09a06d58?s=128

Tristan Hume

March 20, 2018
Tweet

Transcript

  1. Algorithms in Rust Motivations, Tips and Tools By Tristan Hume

  2. 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
  3. Intro: Overview — Intro — What makes Rust good for

    algorithms — Tips: Writing and testing algorithms in Rust — Tools: Lots of handy Rust crates
  4. 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
  5. 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
  6. Why Rust for algorithms?

  7. 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()); } }
  8. 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 { // ... }
  9. 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
  10. Fearless Concurrency use rayon::prelude::*; let mut correlations : Vec<f32> =

    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);
  11. 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 => (), }
  12. Auto-Derive #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] pub struct

    ScopeStack { clear_stack: Vec<Vec<Scope>>, scopes: Vec<Scope>, } impl fmt::Debug for Scope { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let s = self.build_string(); write!(f, "<{}>", s) } }
  13. Print Debugging #[derive(Debug)] struct TheOnlyFakeExample { fake: usize, example: Vec<String>,

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

    TheOnlyFakeExample { fake: 5, example: [ "test\t\n" ] }
  15. Tips/Advice

  16. 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!
  17. 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
  18. 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() }
  19. 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() }
  20. 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<ZipSegment> { 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}) } } } }
  21. 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), } }
  22. 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! "
  23. 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
  24. Tools / Crates

  25. Property Testing #[macro_use] extern crate quickcheck; quickcheck! { fn prop(xs:

    Vec<u32>) -> 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(); } }
  26. 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
  27. 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") }); } }
  28. 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); }); }
  29. 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
  30. Benchmarking criterion can do significance testing to tell you if

    performance improved, and generate fancy reports.
  31. 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
  32. 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();
  33. 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<f32, ScreenSpace>; pub type ScreenSize = TypedSize2D<f32, ScreenSpace>; pub struct WorldSpace; pub type WorldPoint = TypedPoint3D<f32, WorldSpace>; pub type ProjectionMatrix = TypedTransform3D<f32, WorldSpace, ScreenSpace>;
  34. 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);
  35. 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
  36. 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
  37. 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
  38. The End Go forth and write code! (And test, benchmark

    and optimize it) http://thume.ca/talks