$30 off During Our Annual Pro Sale. View Details »

Need for Speed: Boost ruby with FFI

Johnlin
September 11, 2015

Need for Speed: Boost ruby with FFI

Let's make ruby the best language in the world by giving it the speed of C. Ruby's goal is maximize programmer happiness, not the happiness of computers. But sometimes we still need to give some sympathy to the poor machine, especially when our API requests timed out. There're multiple ways to speed up Ruby by running C under the hood. Among them, FFI (foreign function interface) is the one that give us the most pleasant experience. With FFI, we can use C libraries without writing a single line of C code. Better yet, we also get cross platform/implementation support for free. We'll start from how FFI works, then calling a simple c function from ruby. Explore more by bridging C structs to ruby class and handing C pointers. Conclude with a FFI example.

Johnlin

September 11, 2015
Tweet

More Decks by Johnlin

Other Decks in Programming

Transcript

  1. Need for Speed
    Boost Ruby with FFI
    @johnlinvc

    View Slide

  2. Me
    • John Lin ( ྛ⇉ᠳʣ
    • @johnlinvc
    • Head of Algorithm @
    Spoonrocket

    View Slide

  3. What’s SpoonRocket?

    View Slide

  4. The Problem

    View Slide

  5. heroku/router: at=error
    code=H12 

    desc="Request timeout"

    View Slide

  6. Real World® Problem

    View Slide

  7. We Need Speed

    View Slide

  8. Change Language
    • Switch to Go / Scala / Java / C++

    View Slide

  9. View Slide

  10. Go Native
    • C extension
    • FFI

    View Slide

  11. C Extension
    • Write the critical part using Ruby API in C
    • Need compiler on production environment
    • Not portable between Platforms

    View Slide

  12. Foreign Function Interface
    • Mounting C function as Ruby method
    • Call functions using C Library libffi
    • Doesn’t need compiler
    • Portable between platforms

    View Slide

  13. C-ext vs FFI

    View Slide

  14. How Libffi works in C

    View Slide

  15. View Slide

  16. C Library Structure(ELF)
    9/8/2015 https://upload.wikimedia.org/wikipedia/commons/7/77/Elf-layout--en.svg
    ...
    .data
    .rodata
    .text
    Program header table
    ELF header
    Section header table
    {
    {
    https://en.wikipedia.org/wiki/Executable_and_Linkable_Format

    View Slide

  17. Load Library
    void* dlopen(const char* path, int mode);
    • load a shared library file at path & relocate the
    address

    View Slide

  18. Function Address
    λ ~/ nm /usr/lib/libruby.dylib
    0000000000027a75 T _InitVM_Enumerator
    00000000000088de T _Init_Array
    000000000012a65e T _Init_BareVM
    00000000000164e2 T _Init_Bignum
    0000000000037829 T _Init_Binding
    000000000001b08e T _Init_Comparable
    000000000001be5c T _Init_Complex
    000000000013c869 T _Init_Cont
    0000000000020721 T _Init_Dir
    00000000000032ec T _Init_Encoding
    00000000000239a6 T _Init_Enumerable
    000000000002906a T _Init_Enumerator
    000000000002caa7 T _Init_Exception

    View Slide

  19. Find Function Address
    void* dlsym(void* handle, const char* symbol);
    • get address of a symbol from a library

    View Slide

  20. Function call
    with cdecl

    View Slide

  21. Call Function With Address
    In C
    typedef int func(void);
    func * f = (func*) 0x12345566;
    int res = f();

    View Slide

  22. Define Function Signature
    ffi_status
    ffi_prep_cif(
    ffi_cif *cif,
    ffi_abi abi,
    unsigned int nargs,
    ffi_type *rtype,
    ffi_type **atypes);
    Define the signature of a function. cif stands for
    Call InterFace

    View Slide

  23. Invoke Function
    void
    ffi_call(ffi_cif *cif,
    void (*fn)(void),
    void *rvalue,
    void **avalue);
    • ffi_call(): call the actual function by address, with
    return address and input parameters

    View Slide

  24. dlopen()
    dlsym()
    ffi_prep_cif()
    ffi_call()

    View Slide

  25. Ruby-FFI
    • Wrapper of C Library libffi
    • First appeared in Rubinius
    • Maintained by JRuby team
    • Available in MRI, JRuby & Rubinius(same
    interface)

    View Slide

  26. Installing
    • gem install ffi

    View Slide

  27. Hello FFI
    #!/usr/bin/env ruby
    require 'ffi'
    module LIBC
    extend FFI::Library
    ffi_lib FFI::Library::LIBC
    attach_function :printf, [:string], :int
    end
    LIBC.printf("hello FFI\n")

    View Slide

  28. Loading Library
    • Loading Multiple Libs

    ffi_lib "foo", "bar"
    • Loading all possible names of a Lib

    ffi_lib [“foo", “foo-1.2”]

    View Slide

  29. Attach Function
    • Function rename

    attach_function :c_print, :printf, [:string], :int
    • Function with variable length

    attach_function :printf, [:string, :varargs], :int


    View Slide

  30. Supported C Types
    • Integer : singed/unsigned of various length
    • Floating Point: float / double
    • String: NULL terminated char array.
    • Pointer: pointer

    View Slide

  31. Define C Structs
    class Timeval < FFI::Struct
    layout :tv_sec => :ulong,
    :tv_usec => :ulong
    end
    struct timeval {
    unsinged long tv_sec; /* seconds since 1/1/1970 */
    unsinged long tv_usec;/* and microseconds */
    };
    C
    Ruby

    View Slide

  32. Accessing C Struct
    module LibC
    extend FFI::Library
    ffi_lib FFI::Library::LIBC
    attach_function :gettimeofday, [ :pointer, :pointer ], :int
    end
    t = Timeval.new
    LibC.gettimeofday(t.pointer, nil)
    puts "t.tv_sec=#{t[:tv_sec]} t.tv_usec=#{t[:tv_usec]}"
    int gettimeofday(struct timeval * tp, void * tzp);
    C signature
    Ruby

    View Slide

  33. Define Callback
    module LibC
    extend FFI::Library
    ffi_lib FFI::Library::LIBC
    callback :qsort_cmp, [ :pointer, :pointer ], :int
    attach_function :qsort,
    [ :pointer, :ulong, :ulong, :qsort_cmp ], :void
    end
    void qsort(void *base, size_t nel, size_t width,
    int (*compar)(const void *, const void *));
    C signature
    Ruby

    View Slide

  34. Using Callback
    pointer = FFI::MemoryPointer.new(:int, 2)
    ary = [ 2, 1 ]
    pointer.write_array_of_int32(ary)
    size_of_int = 4
    LibC.qsort(pointer, ary.count, size_of_int) do |p1, p2|
    i1 = p1.read_int32
    i2 = p2.read_int32
    i1 < i2 ? -1 : i1 > i2 ? 1 : 0
    end
    p pointer.get_array_of_int32(0, ary.count)

    View Slide

  35. Features
    • Load C library
    • Invoke C function
    • Auto convert between Ruby types & C types
    • Define/Access C Struct
    • Using block/Proc as callback

    View Slide

  36. Benchmark
    • Find Eigenvalue & Eigenvector of a matrix
    • Benchmark the runtime of
    • Ruby Matrix in Stdlib
    • C Extension with LAPACK
    • FFI with LAPACK

    View Slide

  37. Performance
    N=1000 N=10000 N=100000
    Ruby 0.3 2.87 30.3
    FFI 0.02 0.19 2.1
    C-Ext 0.01 0.1 0.99

    View Slide

  38. Take aways
    • Don’t give up ruby if you need speed.
    • How FFI works under the hood
    • FFI core features

    View Slide

  39. Q&A

    View Slide

  40. References
    • Linker & Loaders
    • Libffi
    • ruby-ffi Gem
    • Race car Image
    • x86 call convention

    View Slide