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

Turbo Rails in Rust

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!

Godfrey Chan

December 12, 2015
Tweet

More Decks by Godfrey Chan

Other Decks in Programming

Transcript

  1. ͜Μʹͪ͸
    Hello!

    View full-size slide

  2. ࢲ͸೔ຊޠ͕࿩ͤ·ͤΜ
    I don't speak Japanese

    View full-size slide

  3. αʔόʔΤϥʔͷͨΊ຋༁Ͱ͖·ͤΜͰͨ͠
    αʔόʔΤϥʔͷͨΊ຋༁Ͱ͖·ͤΜͰͨ͠

    View full-size slide

  4. Godfrey Chan
    @chancancode

    View full-size slide

  5. ొஃऀ
    Speaker

    View full-size slide

  6. ొஃऀ
    The Altar Climber?
    Magic
    Ruby
    JS
    C
    Rust

    View full-size slide

  7. ͱʹ͔͘
    Anyway…

    View full-size slide

  8. ຊ෺ͷΞϝϦΧྉཧ
    Authentic American Cuisine
    4 Dec 2015 ≥ Portland Airport

    View full-size slide

  9. 14ºCanada
    Χφμ

    View full-size slide

  10. 14ºCanada
    Χφμ
    "It's 14 degrees in Canada."

    View full-size slide

  11. 57ºFreedom
    ࣗ༝౓

    View full-size slide

  12. 57ºFreedom
    ࣗ༝౓
    "57 degrees of Freedom"

    View full-size slide

  13. gem install canada

    View full-size slide

  14. >> require "canada"
    => true
    >> " ".blank_eh?
    => true
    >> "hi".blank_eh?
    => false

    View full-size slide

  15. Also available for mruby
    ! chancancode/mruby-canada

    View full-size slide

  16. ͱʹ͔͘
    Anyway…

    View full-size slide

  17. Ϡϑμ͸Ͳ͜ʁ
    Where is Yehuda?

    View full-size slide

  18. Lesson*
    *Always check the fineprint!

    View full-size slide

  19. ͱʹ͔͘
    Anyway…

    View full-size slide

  20. High-level, predictable, dynamic, OOP, metaprogramming…

    View full-size slide

  21. Low-level, imperative, dangerous…

    View full-size slide

  22. Fast, closer to The Metal™…

    View full-size slide

  23. Native Extensions
    Best of both worlds*

    View full-size slide

  24. gem install json

    View full-size slide

  25. gem install json
    JSON::Ext
    Native Extension
    JSON::Pure
    Pure Ruby

    View full-size slide

  26. Native Extensions
    Best of both worlds*

    View full-size slide

  27. Native Extensions
    Best of both worlds*
    * For the end-user

    View full-size slide

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

    View full-size slide

  29. "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

    View full-size slide

  30. @SamSaffron

    View full-size slide

  31. String#blank?

    View full-size slide

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

    View full-size slide

  33. ! SamSaffron/fast_blank

    View full-size slide

  34. Up to 20x faster

    View full-size slide


  35. Knows just enough C to be dangerous

    View full-size slide


  36. Knows just enough C to be dangerous
    ptr

    View full-size slide


  37. Knows just enough C to be dangerous
    *ptr

    View full-size slide


  38. Knows just enough C to be dangerous
    ***********ptr

    View full-size slide


  39. Knows just enough C to be dangerous
    &&&&&&&ptr

    View full-size slide

  40. Yehuda Katz
    @wycats

    View full-size slide

  41. Hack Without Fear!

    View full-size slide

  42. Structs
    struct Circle {
    radius: f64
    }
    impl Circle {
    fn new(r: f64) -> Circle {
    Circle { radius: r }
    }
    fn diameter(&self) -> f64 {
    self.radius * 2
    }
    }

    View full-size slide

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

    View full-size slide

  44. Static Dispatch

    View full-size slide

  45. Static Dispatch Allows Function Inlining

    View full-size slide

  46. Function Inlining (Wikipedia)
    the primary benefit of inline
    expansion is to allow further
    optimizations

    View full-size slide

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




    View full-size slide

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

    View full-size slide

  49. Stack Allocation
    • The stack pointer handles freeing memory
    (fast)
    • In Ruby, it is an optimization, never guaranteed

    View full-size slide

  50. But!
    • The exact size for each function must be known
    at compile time
    • It's dangerous to give out pointers to the stack

    View full-size slide

  51. Rust ownership makes stack allocation safe

    View full-size slide

  52. Traits and Generics

    View full-size slide

  53. Traits and Generics
    This is where it gets interesting!

    View full-size slide

  54. Traits
    struct Circle {
    radius: f64
    }
    struct Square {
    side: f64
    }

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  57. Traits Are Where The Magic Is
    fn print_area(shape: &S) {
    println!("{}", shape.area());
    }

    View full-size slide

  58. Still Static Dispatch!

    View full-size slide

  59. It adds up... a lot!

    View full-size slide

  60. Back to fast_blank

    View full-size slide

  61. 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;
    }

    View full-size slide

  62. In Rust
    pub extern "C" fn fast_blank(buf: Buf) -> bool {
    buf.as_slice().chars().all(|c| c.is_whitespace())
    }

    View full-size slide

  63. =========== 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

    View full-size slide

  64. pub extern "C" fn fast_blank(buf: Buf) -> bool {
    buf.as_slice().chars().all(|c| c.is_whitespace())
    }

    View full-size slide

  65. pub extern "C" fn fast_blank(buf: Buf) -> bool {
    buf.as_slice().chars().all(|c| c.is_whitespace())
    }
    *

    View full-size slide

  66. 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
    #include
    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

    View full-size slide

  67. // 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
    #include
    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
    #include
    #include
    #include
    #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

    View full-size slide

  68. 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::("blank?", string_is_blank);
    }
    Rust Extension
    #include
    #include
    #include
    #include
    #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

    View full-size slide

  69. 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::("blank?", string_is_blank);
    }

    View full-size slide

  70. extern
    use libcruby
    use libcruby
    extern
    if
    }
    }
    }
    #[no_mangle]
    pub extern
    String.
    }
    -> Boolean
    Qtrue
    Qfalse
    libcruby::types::Boolean

    View full-size slide

  71. extern
    use libcruby
    use libcruby
    extern
    if
    }
    }
    }
    #[no_mangle]
    pub extern
    String.
    }
    String.define_method::("blank?", string_is_blank);

    View full-size slide

  72. String.define_method::("blank?", string_is_blank);

    View full-size slide

  73. String.
    String
    libcruby::consts::String

    View full-size slide

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

    View full-size slide

  75. String.
    String
    extern
    }
    #[link_name = "rb_cString"]
    pub static String: Class<'static>;
    libcruby::consts::String

    View full-size slide

  76. String.
    String
    extern
    }
    rb_cString
    libcruby::consts::String

    View full-size slide

  77. String.
    String
    extern
    }
    Class
    libcruby::consts::String

    View full-size slide

  78. String.
    String
    extern
    }
    Class
    libcruby::consts::String
    libcruby::types::Class

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  82. String.define_method::("blank?", string_is_blank);
    rb_define_method_id(rb_cString, rb_intern("blank?"), string_is_blank, 0);

    View full-size slide

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

    View full-size slide

  84. let str = String::new("zomg");
    let quoted = str.send::("inspect");
    let obj_id = str.send::("__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__"));

    View full-size slide

  85. trb files
    class String
    def blank? -> bool
    <<-RUST
    self[..].chars().all(|c| c.is_whitespace())
    RUST
    end
    end

    View full-size slide

  86. Usage
    $ ruby extconf.rb
    $ make
    $ ruby -r ./fast_blank -e "p ' '.blank?"
    true
    $ ruby -r ./fast_blank -e "p ' nope '.blank?"
    false

    View full-size slide

  87. trb + libcruby

    View full-size slide

  88. Turbo Rails with Rust?

    View full-size slide

  89. What about Ruby?

    View full-size slide

  90. Channels
    Forbid Aliasing
    Functional
    Forbid Mutation
    Rust
    Forbid
    Both at
    the Same
    Time
    Shared or Mutable, Pick One

    View full-size slide

  91. ͋Γ͕ͱ͏
    Thank you!

    View full-size slide