Need for Speed: Boost ruby with FFI

3f7d9611fc919c98512b779cde637dfc?s=47 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.

3f7d9611fc919c98512b779cde637dfc?s=128

Johnlin

September 11, 2015
Tweet

Transcript

  1. Need for Speed Boost Ruby with FFI @johnlinvc

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

    of Algorithm @ Spoonrocket
  3. What’s SpoonRocket?

  4. The Problem

  5. heroku/router: at=error code=H12 
 desc="Request timeout"

  6. Real World® Problem

  7. We Need Speed

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

    / C++
  9. None
  10. Go Native • C extension • FFI

  11. C Extension • Write the critical part using Ruby API

    in C • Need compiler on production environment • Not portable between Platforms
  12. Foreign Function Interface • Mounting C function as Ruby method

    • Call functions using C Library libffi • Doesn’t need compiler • Portable between platforms
  13. C-ext vs FFI

  14. How Libffi works in C

  15. None
  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
  17. Load Library void* dlopen(const char* path, int mode); • load

    a shared library file at path & relocate the address
  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
  19. Find Function Address void* dlsym(void* handle, const char* symbol); •

    get address of a symbol from a library
  20. Function call with cdecl

  21. Call Function With Address In C typedef int func(void); func

    * f = (func*) 0x12345566; int res = f();
  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
  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
  24. dlopen() dlsym() ffi_prep_cif() ffi_call()

  25. Ruby-FFI • Wrapper of C Library libffi • First appeared

    in Rubinius • Maintained by JRuby team • Available in MRI, JRuby & Rubinius(same interface)
  26. Installing • gem install ffi

  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")
  28. Loading Library • Loading Multiple Libs
 ffi_lib "foo", "bar" •

    Loading all possible names of a Lib
 ffi_lib [“foo", “foo-1.2”]
  29. Attach Function • Function rename
 attach_function :c_print, :printf, [:string], :int

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

  30. Supported C Types • Integer : singed/unsigned of various length

    • Floating Point: float / double • String: NULL terminated char array. • Pointer: pointer
  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
  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
  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
  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)
  35. Features • Load C library • Invoke C function •

    Auto convert between Ruby types & C types • Define/Access C Struct • Using block/Proc as callback
  36. Benchmark • Find Eigenvalue & Eigenvector of a matrix •

    Benchmark the runtime of • Ruby Matrix in Stdlib • C Extension with LAPACK • FFI with LAPACK
  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
  38. Take aways • Don’t give up ruby if you need

    speed. • How FFI works under the hood • FFI core features
  39. Q&A

  40. References • Linker & Loaders • Libffi • ruby-ffi Gem

    • Race car Image • x86 call convention