Native MRI Callback
A few weeks ago after a discussion with raggi (yet again) about callback implementations for the Ruby language, we kicked off an attempt at a minimal native object that’s very close in performance to method dispatch.
Here’s a representation of the pure ruby version :
The game plan
The following inefficiencies would have to be addressed :
- Instance variables Instance variable lookups in general require some symbol table overhead.
- Block procedure arguments Block proc arguments are up to 6x slower than passing explicit objects.
- Method dispatch The splat operator and __send__, although efficient, does have some noticeable overheads.
- Kernel convenience method
Additional method dispatch in the critical path through this global function.
Ivar lookup overhead is mostly negated for Ruby 1.9 as the RObject struct stores the first three ivars for each object.
Implementation
The callback structure
We introduce a new callback struct with 2 members :
- object Ruby object to invoke the callback on
- method Method to call.Defaults to #call if no method argument given
Wrapping the C struct as T_DATA has the least overheads in accessing either the object and the method when our callback fires.
GC integration’s a piece of cake as well.
Allocation
Wrapping C structures requires an allocator function (Callback.allocate) to initialize our callback struct and register our mark and free functions with the GC.This is usually invoked prior to #initialize and is transparent for our use cases.
Definition
Three definition (setup) styles supported :
- Object and method arguments
- Block argument
- Farmed off an existing object
We handle each of the argument or block definition styles in an initialize method and handle edge cases such as not assigning a callback method that’s not defined for the object.It’s also important to ensure that our callback is frozen at this point ( I’m looking at you lifo, this one’s legit
) for consistency.
Our global function, Callback(), which is defined much like the popular Array() and Float() coercion helpers, and the farm out API reuse the alloc and init covered above.
Results
An intuitive callback API that’s slower than method dispatch, but faster than lambda callbacks.
Have a look at the source or install the gem to play around.
sudo gem install methodmissing-callback
Tested on MRI versions > 1.8.6 and 1.9.2 – please have a look through the test suite for further examples.
Follow me @ github or twitter if you enjoyed this article.Thanks for reading and special thanks to Aman Gupta for review !
