Turbo Rails in Rust

22bb3e56828870ee9a0dd93aeadbe04a?s=47 Godfrey Chan
December 12, 2015

Turbo Rails in Rust

Ruby is not the fastest language in the world, there is no doubt about it. This doesn't turn out to matter all that much – Ruby and its ecosystem has so much more to offer, making it a worthwhile tradeoff a lot of the times.

However, you might occasionally encounter workloads that are simply not suitable for Ruby. In this talk, we will explore building a native extension with Rust to speed up parts of Rails. (No prior experience with Rust required!) What does Rust has to offer in this scenario over plain-old C? Let's find out!

22bb3e56828870ee9a0dd93aeadbe04a?s=128

Godfrey Chan

December 12, 2015
Tweet

Transcript

  1. 5.
  2. 9.

    "

  3. 11.

    #

  4. 12.
  5. 13.
  6. 14.
  7. 17.
  8. 18.
  9. 25.
  10. 30.
  11. 32.
  12. 33.

    C

  13. 41.
  14. 42.

    "Here’s a general value for Rails development: We will jump

    through hoops on the implementation to make the user-facing API nicer." David Heinemeier Hansson Creator of Ruby on Rails
  15. 43.

    "Here’s a general value for Rails development: We will jump

    through hoops on the implementation to make the user-facing API nicer." David Heinemeier Hansson Creator of Ruby on Rails developer experience
  16. 46.

    class String # A string is blank if it's empty

    or contains whitespaces only: # # ''.blank? # => true # ' '.blank? # => true # "\t\n\r".blank? # => true # ' blah '.blank? # => false # # Unicode whitespace is supported: # # "\u00a0".blank? # => true # # @return [true, false] def blank? /\A[[:space:]]*\z/ === self end end
  17. 49.

    Me

  18. 55.
  19. 56.
  20. 57.
  21. 59.
  22. 60.
  23. 62.

    Structs struct Circle { radius: f64 } impl Circle {

    fn new(r: f64) -> Circle { Circle { radius: r } } fn diameter(&self) -> f64 { self.radius * 2 } }
  24. 63.

    Structs struct Circle { radius: f64 } impl Circle {

    fn new(r: f64) -> Circle { Circle { radius: r } } fn diameter(&self) -> f64 { self.radius * 2 } } fn main() { let c = Circle::new(10.0); println!("{}", c.diameter()); }
  25. 68.

    Heap Allocation typedef struct { double side; } square; int

    main() { square* s = (square*)malloc(sizeof(square)); s->side = 10.0; printf("%f", diagonal(s)); free(square); }
  26. 69.

    Stack Allocation typedef struct { double side; } square; int

    main() { square s; s.side = 10.0; printf("%f", diagonal(s)); } "Stack" square diagonal(s) diagonal(s) main() {side: 10.0} 14.142135623
  27. 70.

    Stack Allocation • The stack pointer handles freeing memory (fast)

    • In Ruby, it is an optimization, never guaranteed
  28. 71.

    But! • The exact size for each function must be

    known at compile time • It's dangerous to give out pointers to the stack
  29. 76.

    Traits struct Circle { radius: f64 } struct Square {

    side: f64 } trait Shape { fn area(&self) -> f64; } impl Shape for Circle { fn area(&self) -> f64 { PI * self.radius.powf(2.0) } } impl Shape for Square { fn area(&self) -> f64 { self.side * self.side } }
  30. 77.

    Traits struct Circle { ... } struct Square { ...

    } trait Shape { ... } impl Shape for Circle { fn area(&self) -> f64 { PI * self.radius.powf(2.0) } } impl Shape for Square { fn area(&self) -> f64 { self.side * self.side } } fn main() { let s = Square::new(10.0); let c = Circle::new(10.0); println!("{}", s.area()); println!("{}", c.area()); }
  31. 82.

    In C static VALUE rb_string_blank(VALUE str) { rb_encoding *enc; char

    *s, *e; enc = STR_ENC_GET(str); s = RSTRING_PTR(str); if (!s || RSTRING_LEN(str) == 0) return Qtrue; e = RSTRING_END(str); while (s < e) { int n; unsigned int cc = rb_enc_codepoint_len(...); switch (cc) { case 9: case 0xa: case 0xb: case 0xc: case 0xd: case 0x20: case 0x85: case 0xa0: case 0x1680: case 0x2000: case 0x2001: case 0x2002: case 0x2003: case 0x2004: case 0x2005: case 0x2006: case 0x2007: case 0x2008: case 0x2009: case 0x200a: case 0x2028: case 0x2029: case 0x202f: case 0x205f: case 0x3000: break; default: return Qfalse; } s += n; } return Qtrue; }
  32. 83.

    In Rust pub extern "C" fn fast_blank(buf: Buf) -> bool

    { buf.as_slice().chars().all(|c| c.is_whitespace()) }
  33. 84.

    =========== Test String Length: 6 =========== Rust 11.043M (± 3.5%)

    i/s - 54.744M C 10.583M (± 8.5%) i/s - 52.464M Ruby 964.469k (±27.6%) i/s - 4.390M
  34. 87.
  35. 88.

    extern crate libc; use std::{ptr, slice}; use std::marker::PhantomData; #[repr(C)] #[allow(raw_pointer_derive)]

    #[derive(Copy, Clone)] pub struct Buf<'a> { ptr: *mut u8, len: usize, marker: PhantomData<&'a ()>, } impl<'a> Buf<'a> { pub fn as_slice(self) -> &'a str { unsafe { let s = slice::from_raw_parts(self.ptr as *const u8, self.len); std::str::from_utf8_unchecked(s) } } } #[no_mangle] pub extern "C" fn fast_blank(buf: Buf) -> bool { buf.as_slice().unwrap().chars().all(|c| c.is_whitespace()) } #include <ruby.h> #include <ruby/encoding.h> typedef struct { void* data; size_t len; } fast_blank_buf_t; static inline fast_blank_buf_t STR2BUF(VALUE str) { return (fast_blank_buf_t) { .data = RSTRING_PTR(str), .len = RSTRING_LEN(str), }; } int fast_blank(fast_blank_buf_t); static VALUE rb_string_blank_p(VALUE str) { return fast_blank(STR2BUF(str)) ? Qtrue : Qfalse; } void Init_fast_blank() { rb_define_method(rb_cString, "blank?", rb_string_blank_p, 0); } Rust Code C Code
  36. 89.

    // Rust Code extern crate libc; use std::{ptr, slice}; use

    std::marker::PhantomData; #[repr(C)] #[allow(raw_pointer_derive)] #[derive(Copy, Clone)] pub struct Buf<'a> { ptr: *mut u8, len: usize, marker: PhantomData<&'a ()>, } impl<'a> Buf<'a> { pub fn as_slice(self) -> &'a str { unsafe { let s = slice::from_raw_parts(self.ptr as *const u8, self.len); std::str::from_utf8_unchecked(s) } } } #[no_mangle] pub extern "C" fn fast_blank(buf: Buf) -> bool { buf.as_slice().unwrap().chars().all(|c| c.is_whitespace()) } // C Code #include <ruby.h> #include <ruby/encoding.h> typedef struct { void* data; size_t len; } fast_blank_buf_t; static inline fast_blank_buf_t STR2BUF(VALUE str) { return (fast_blank_buf_t) { .data = RSTRING_PTR(str), .len = RSTRING_LEN(str), }; } int fast_blank(fast_blank_buf_t); static VALUE rb_string_blank_p(VALUE str) { return fast_blank(STR2BUF(str)) ? Qtrue : Qfalse; } void Init_fast_blank() { rb_define_method(rb_cString, "blank?", rb_string_blank_p, 0); } Rust Extension #include <stdio.h> #include <ruby.h> #include <ruby/encoding.h> #include <ruby/re.h> #define STR_ENC_GET(str) rb_enc_from_index(ENCODING_GET(str)) static VALUE rb_string_blank(VALUE str) { rb_encoding *enc; char *s, *e; enc = STR_ENC_GET(str); s = RSTRING_PTR(str); if (!s || RSTRING_LEN(str) == 0) return Qtrue; e = RSTRING_END(str); while (s < e) { int n; unsigned int cc = rb_enc_codepoint_len(s, e, &n, enc); switch (cc) { case 9: case 0xa: case 0xb: case 0xc: case 0xd: case 0x20: case 0x85: case 0xa0: case 0x1680: case 0x2000: case 0x2001: case 0x2002: case 0x2003: case 0x2004: case 0x2005: case 0x2006: case 0x2007: case 0x2008: case 0x2009: case 0x200a: case 0x2028: case 0x2029: case 0x202f: case 0x205f: case 0x3000: break; default: return Qfalse; } s += n; } return Qtrue; } void Init_fast_blank() { rb_define_method(rb_cString, "blank?", rb_string_blank, 0); } C Extension
  37. 90.
  38. 91.

    extern crate libcruby; use libcruby::consts::*; use libcruby::types::*; extern "C" fn

    string_is_blank(receiver: String) -> Boolean { if receiver[..].chars().all(|c| c.is_whitespace()) { Qtrue } else { Qfalse } } #[no_mangle] pub extern "C" fn Init_fast_blank() { String.define_method::<String, Boolean>("blank?", string_is_blank); } Rust Extension #include <stdio.h> #include <ruby.h> #include <ruby/encoding.h> #include <ruby/re.h> #define STR_ENC_GET(str) rb_enc_from_index(ENCODING_GET(str)) static VALUE rb_string_blank(VALUE str) { rb_encoding *enc; char *s, *e; enc = STR_ENC_GET(str); s = RSTRING_PTR(str); if (!s || RSTRING_LEN(str) == 0) return Qtrue; e = RSTRING_END(str); while (s < e) { int n; unsigned int cc = rb_enc_codepoint_len(s, e, &n, enc); switch (cc) { case 9: case 0xa: case 0xb: case 0xc: case 0xd: case 0x20: case 0x85: case 0xa0: case 0x1680: case 0x2000: case 0x2001: case 0x2002: case 0x2003: case 0x2004: case 0x2005: case 0x2006: case 0x2007: case 0x2008: case 0x2009: case 0x200a: case 0x2028: case 0x2029: case 0x202f: case 0x205f: case 0x3000: break; default: return Qfalse; } s += n; } return Qtrue; } void Init_fast_blank() { rb_define_method(rb_cString, "blank?", rb_string_blank, 0); } C Extension
  39. 92.

    extern crate libcruby; use libcruby::consts::*; use libcruby::types::*; extern "C" fn

    string_is_blank(receiver: String) -> Boolean { if receiver[..].chars().all(|c| c.is_whitespace()) { Qtrue } else { Qfalse } } #[no_mangle] pub extern "C" fn Init_fast_blank() { String.define_method::<String, Boolean>("blank?", string_is_blank); }
  40. 93.

    extern use libcruby use libcruby extern if } } }

    #[no_mangle] pub extern String. } -> Boolean Qtrue Qfalse libcruby::types::Boolean
  41. 94.

    extern use libcruby use libcruby extern if } } }

    #[no_mangle] pub extern String. } String.define_method::<String, Boolean>("blank?", string_is_blank);
  42. 97.

    String. String extern { #[link_name = "rb_Qfalse"] pub static Qfalse:

    Boolean<'static>; #[link_name = "rb_Qtrue"] pub static Qtrue: Boolean<'static>; // ... #[link_name = "rb_cString"] pub static String: Class<'static>; #[link_name = "rb_mKernel"] pub static Kernel: Module<'static>; } libcruby::consts::String
  43. 98.
  44. 102.

    String. .define_method extern { fn rb_define_method_id(receiver: VALUE, name: ID, func:

    *const c_void, argc: c_int); } #[repr(C)] pub struct Class { VALUE: VALUE } #[repr(C)] pub struct Module { VALUE: VALUE } trait Namespace { fn define_method(&self, name: &str, func: ...) { unsafe { rb_define_method_id(self.as_ptr(), intern(name), func); } } } impl Namespace for Module {} impl Namespace for Class {}
  45. 103.

    String. .define_method extern { fn rb_define_method_id } #[repr(C)] pub struct

    Class VALUE } #[repr(C)] pub struct Module VALUE } trait Namespace fn define_method } } impl Namespace impl Namespace trait Namespace { fn define_method(&self, name: &str, func: ...) { } } impl Namespace for Class {}
  46. 104.

    String. .define_method extern { fn rb_define_method_id } #[repr(C)] pub struct

    Class VALUE } #[repr(C)] pub struct Module VALUE } trait Namespace fn define_method } } impl Namespace impl Namespace extern { fn rb_define_method_id(receiver: VALUE, name: ID, func: *const c_void, argc: c_int); } rb_define_method_id(self.as_ptr(), intern(name), func);
  47. 107.

    let str = String::new("zomg"); let quoted = str.send::<String>("inspect"); let obj_id

    = str.send::<Fixnum>("__id__"); VALUE str = rb_str_new_cstr("zomg"); VALUE quoted = rb_func_call(str, rb_intern("inspect")); VALUE obj_id = rb_func_call(str, rb_intern("__id__"));
  48. 109.

    Usage $ ruby extconf.rb $ make $ ruby -r ./fast_blank

    -e "p ' '.blank?" true $ ruby -r ./fast_blank -e "p ' nope '.blank?" false
  49. 113.