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

    View full-size slide

  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

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  6. Why Rust for algorithms?

    View full-size slide

  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());
    }
    }

    View full-size slide

  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 {
    // ...
    }

    View full-size slide

  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

    View full-size slide

  10. 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);

    View full-size slide

  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 => (),
    }

    View full-size slide

  12. 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)
    }
    }

    View full-size slide

  13. 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
    }

    View full-size slide

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

    View full-size slide

  15. 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!

    View full-size slide

  16. 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

    View full-size slide

  17. 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()
    }

    View full-size slide

  18. 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()
    }

    View full-size slide

  19. 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})
    }
    }
    }
    }

    View full-size slide

  20. 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),
    }
    }

    View full-size slide

  21. 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! "

    View full-size slide

  22. 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

    View full-size slide

  23. Tools / Crates

    View full-size slide

  24. 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();
    }
    }

    View full-size slide

  25. 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

    View full-size slide

  26. 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")
    });
    }
    }

    View full-size slide

  27. 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);
    });
    }

    View full-size slide

  28. 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

    View full-size slide

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

    View full-size slide

  30. 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

    View full-size slide

  31. 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();

    View full-size slide

  32. 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;

    View full-size slide

  33. 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);

    View full-size slide

  34. 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

    View full-size slide

  35. 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

    View full-size slide

  36. 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

    View full-size slide

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

    View full-size slide