Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Algorithms in Rust

Algorithms in Rust

Talk given at the Bay Area Rust Meetup

Tristan Hume

March 20, 2018
Tweet

More Decks by Tristan Hume

Other Decks in Programming

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