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 !

5 Responses to “Native MRI Callback”

  1. Compilação de Pequenas Novidades do Mundo Ruby - Setembro de 2009  on September 10th, 2009

    [...] caras da MethodMissing desenvolveram uma  biblioteca Ruby inteligente que oferece um sistema de callbacks nativo que tem performance superior aos lambdas,  (embora não tão rápido quando o dispatch de métodos [...]

  2. roger  on September 11th, 2009

    I like it. Interesting. Unfortunately at least on my browser the results are hard to read.
    Thanks!
    -r

  3. roger  on October 12th, 2009

    As a note, your pure ruby example doesn’t allow for
    ‘hai’.callback(:gsub)

    Anyway, here’s my wish list for Callback, if you don’t mind the suggestions:

    class A
    def go
    callback(:go) # callback to self.go
    a = ”
    a.callback(:to_s) # a.to_s
    a.callback { to_s } # instance_eval’ed for a when it is call’ed
    c = callback(:go, ‘param1′, ‘param2′) # can pass it parameters to use so you can call it like
    c.call # calls a.go(‘param1′, ‘param2′)
    c.call(‘param3′) # calls a.go(‘param1′, ‘param2′, ‘parame’) — either that or be disallowed or replace the original params, like a.go(‘param1′) :)

    Callback(A, :go) # this one works currently
    A.callback(:class_method)
    end

    def A.class_method
    end

    end

    Also shouldn’t
    Callback { }

    just be an alias for proc { }?

    Also interesting would be if you could pass it for a block parameter, like:

    >> A.go &Callback(A, :class_method)
    TypeError: wrong argument type Callback (expected Proc)
    from (irb):16
    from :0

    I guess you could fake it with
    class Callback
    def to_proc
    @proc ||= proc {|*args| call *args}
    end
    end

    however this still requires creating a proc at least once…which is slow, if I remember correctly.

    If you knew a class and its methods were going to stick around forever, you could cache callbacks to each class, but not for instances you couldn’t cache.

    Anyway I like it :)

    -=r

  4. roger  on October 13th, 2009

    Looks like the link to the source should be
    http://github.com/methodmissing/cb

  5. Interesting Ruby Tidbits That Don’t Need Separate Posts #28  on February 13th, 2011

    [...] guys at MethodMissing have come up with a clever Ruby library that provides a native callback system that offers performance superior to lambdas (though not as fast as regular method dispatch). Some [...]


Leave a Reply