goffing around with ruby #rubyconfph
TRANSCRIPT
GoFFIng around with Ruby@gautamrege
@joshsoftware
FFI
Foreign Function Interface
*In stdlib since Ruby 1.9.3
Why do we need FFI?• Resque / Sidekiq / delayed_job
• 3rd party API requests
• Sending newsletters
• Heavy computation
• Fast searches, aggregation.
Why Go?• Power of multi-core.
• Goroutines to the rescue
• Language is restricted focussed.
• Programming Ethics
• Compiled
If it compiles, it works
What is FFI anyway?• Language interoperability!
• Calling convention.
• libffi.so
• ruby-ffi
• Fiddle (in Stdlib since 1.9.3)
FFI or Good ol’ C-ext?• FFI Easier to write and maintain.
• Gems are now easier to install - don’t need build dependencies.
• Portable code
• No more “Building native extensions…” while installing gems.
Simple Ruby Codedef race(legs, benchmark) tids = [] legs.times do |i| tids << Thread.new(i) do |leg| array = (1..benchmark).map { |i| i } p "Done leg #{leg} benchmark #{benchmark}" end end tids.each { |t| t.join }end
race(4, 50_000_000)
Benchmark - Ruby$ time ruby run1.rb Done leg 1 benchmark 50000000 Done leg 0 benchmark 50000000 Done leg 3 benchmark 50000000 Done leg 2 benchmark 50000000
real 0m18.242s user 0m16.626s sys 0m1.292s
Simple Go Codepackage main
import "fmt"
func race(leg int, benchmark int) {arr := []int{}for i := 0; i < benchmark; i++ {arr = append(arr, i)
}fmt.Println("Done leg:", leg)
}
func main() {for i := 0; i < 4; i++ {race(i, 50000000)
}}
Simple Go Codepackage main
import "fmt"
func race(leg int, benchmark int) {arr := []int{}for i := 0; i < benchmark; i++ {arr = append(arr, i)
}fmt.Println("Done leg:", leg)
}
func main() {for i := 0; i < 4; i++ {race(i, 50000000)
}}
Simple Go Codepackage main
import "fmt"
func race(leg int, benchmark int) {arr := []int{}for i := 0; i < benchmark; i++ {arr = append(arr, i)
}fmt.Println("Done leg:", leg)
}
func main() {for i := 0; i < 4; i++ {race(i, 50000000)
}}
Simple Go Codepackage main
import "fmt"
func race(leg int, benchmark int) {arr := []int{}for i := 0; i < benchmark; i++ {arr = append(arr, i)
}fmt.Println("Done leg:", leg)
}
func main() {for i := 0; i < 4; i++ {race(i, 50000000)
}}
Benchmark - Go$ time go run run1.go Done leg: 0 Done leg: 1 Done leg: 2 Done leg: 3
real 0m2.774s user 0m2.564s sys 0m0.929s
Calling Go from Ruby
• Build a C library using Go code.
• Build a FFI extension in Ruby
• Call it !
c-shared Go librarypackage main
import ("C""fmt"
)
func race(leg int, benchmark int) {arr := []int{}for i := 0; i < benchmark; i++ {
arr = append(arr, i)}fmt.Println("Done leg:", leg)
}
//export churnfunc churn(leg C.int, benchmark C.int) {
for i := 0; i < int(leg); i++ {race(i, int(benchmark))
}}
func main() {}
c-shared Go librarypackage main
import ("C""fmt"
)
func race(leg int, benchmark int) {arr := []int{}for i := 0; i < benchmark; i++ {
arr = append(arr, i)}fmt.Println("Done leg:", leg)
}
//export churnfunc churn(leg C.int, benchmark C.int) {
for i := 0; i < int(leg); i++ {race(i, int(benchmark))
}}
func main() {}
c-shared Go librarypackage main
import ("C""fmt"
)
func race(leg int, benchmark int) {arr := []int{}for i := 0; i < benchmark; i++ {
arr = append(arr, i)}fmt.Println("Done leg:", leg)
}
//export churnfunc churn(leg C.int, benchmark C.int) {
for i := 0; i < int(leg); i++ {race(i, int(benchmark))
}}
func main() {}
$ go build -o librun.so -buildmode=c-shared run2.go
Ruby FFI extensionrequire 'fiddle'
module Fun def self.race(leg, benchmark) librun = Fiddle.dlopen(‘./librun.so')
race = Fiddle::Function.new( librun['churn'], [Fiddle::TYPE_INT, Fiddle::TYPE_INT], Fiddle::TYPE_VOID )
race.call(leg, benchmark) endend
Call it! require './fun'Fun.race(4, 50_000_000)
$ time ruby run2.rb Done leg: 0 Benchmark 50000000 Done leg: 1 Benchmark 50000000 Done leg: 2 Benchmark 50000000 Done leg: 3 Benchmark 50000000
real 0m2.776s user 0m2.719s sys 0m0.969s
Practical Example
Practical ExampleMake Model Variant
Practical ExampleMake Model Variant
Practical Example
Practical Example
Insurance Vendor
Practical Example
Insurance Vendor
68 columns
Doing the Math!
18 STATES428 CARS
68 VENDORS
= 523,872 cells
If that was not enough …For each of the 523,872 cells
Calculate DepreciationIndex in ElasticSearch
for cmake, v := range segments {for model, j := range v {for submodel, k := range j {for insurance, segment := range k {wg.Add(1)go func(cmake string, model string, submodel string, insurance string, segment string) {
defer wg.Done()// data := es_data{...}// prepare_es_data(...)es_add_depreciation(&data)
}(cmake, model, submodel, insurance, segment)}
}}
}
Practical Example
About 350,000 go-routines
for cmake, v := range segments {for model, j := range v {for submodel, k := range j {for insurance, segment := range k {wg.Add(1)go func(cmake string, model string, submodel string, insurance string, segment string) {
defer wg.Done()// data := es_data{...}// prepare_es_data(...)es_add_depreciation(&data)
}(cmake, model, submodel, insurance, segment)}
}}
}
Practical Example
About 350,000 go-routines
Performance Benefits
Performance Benefits• Sidekiq: 2 GB memory & ~18 minutes
Performance Benefits• Sidekiq: 2 GB memory & ~18 minutes
• Go FFI: 135 MB memory & 1 minute 13 seconds.
Performance Benefits• Sidekiq: 2 GB memory & ~18 minutes
• Go FFI: 135 MB memory & 1 minute 13 seconds.
ElasticSearch indexing took 48 seconds
Complexities of FFI• Memory Management (alloc / free)
• Pointer management
• Fiddle::Pointer
• Fiddle::Closure
Don’t use them unless you HAVE to!
Resources• Fiddle http://ruby-doc.org/stdlib-2.0.0/libdoc/fiddle/rdoc/
Fiddle.html
• FFI Core Concepts https://github.com/ffi/ffi/wiki/Core-Concepts
• Github: ffi/ffi
• Effective Go ( https://golang.org/doc/effective_go.html )
• A Tour of Go ( https://tour.golang.org/welcome/1 )
@joshsoftware
@gautamrege
http://www.codecuriosity.org