Updated 2012-01-11 03:37:57 by RLE

MS: While playing with tcl-only OO, an important design decision concerns the method used for dipatching the calls to the different object methods. The speed of the system, and ultimately its usability, is strongly dependent on the speed of the dispatcher.

So I decided to time the different possible methods (marked with *) and contrast the timings with plain proc calls.

In order to allow dynamic class membership, the complete path to any object method cannot be fixed at compile time. It hence has to be looked up at runtime. The dispatcher's task is to do this lookup for arbitrary method calls, and call the correct method.

Let me describe a couple of variants that occurred to me (and kindly request any other suggestions!). The framework is as follows:

  • there is a class "dad", residing in namespace "::dad"
  • the class has a (here trivial) method "proc test {x args} {set x}"
  • an object "son" of class "dad" resides in namespace "::son"
  • the user wants to call son's test

We focus here on the speed of dispatching; a different (less important?) concern is the speed at which class membership relationships can be updated. Another concern that we disregard here is the memory requirements for each object.

The methods that we compare here are:

  1. Plain proc call Just copy the proc ::dad::test to ::son::test . Updating the class proc is a real pain with this method, adding class procs and changing class membership too.
  2. namespace import Import ::dad::test into ::son . . Updating is very easy, changing class membership is easy, adding class procs is a pain. (cobj2)
  3. interp alias Alias ::son::test to ::dad::test . Similar properties to 2. above.
  4. indirect through proc Define ::son::test to call ::dad::test . Similar properties to 2. above.
  5. store in array Keep a global array to redirect the calls (I think stooop [1] uses this approach). Similar properties to 2. above.
  6. switch Store the redirects in the body of an object dispatcher, which switches according to name. (gadgets, LOST, cobj). Updating class procs is easy, changing class membership less so, adding class procs is a pain.
  7. lookup Son knows his heritage; it's dispatcher looks in the ancestors until it finds a suitable proc (Chaining things). Updating class procs, adding class procs and changing class membership is VERY easy.
  8. inline For comparison, to estimate how much time was spent actually performing the proc
  9. noop To see how much loop overhead there is in the measurement

If anyone ever wants to update this, then namespace ensemble and tclOO dispatch should be added too.

DKF: I expect that ensembles will be similar to interp alias, and that TclOO's normal proc dispatch will be a bit slower than a procedure call. Whatever that means.

Remark that methods 1-5 call the target proc ONLY; they do not need to eval their args beforehand. Methods 6 and 7 on the other hand have to eval.

NOTE: this has been modified to make it fairer and easier to read: (a) the evaling methods now eval a pure list (faster!); (b) we use time instead of a for loop - less loop overhead, purer measurement and result is directly in [microseconds per call]; (c) added for comparison the [lookup] and [noop] results; (d) increased the runs to 30000, which produces much stabler results on my machine.

How the methods scale for deeper inheritance may also be interesting; not done here.

On my notebook (P166, linux, tcl8.4a2) the timings for the script below are:
 % source sent_corr
direct 16 microseconds per iteration
import 16 microseconds per iteration
interp alias 51 microseconds per iteration
indirect 67 microseconds per iteration
array 27 microseconds per iteration
switch 99 microseconds per iteration
lookup 223 microseconds per iteration
inline 7 microseconds per iteration
noop 3 microseconds per iteration


HK: Tried this benchmark on a Sun Blade 1500 running on tcl8.4.6. Here are the timings:
direct 2 microseconds per iteration
import 3 microseconds per iteration
interp alias 3 microseconds per iteration
indirect 10 microseconds per iteration
array 3 microseconds per iteration
switch 13 microseconds per iteration
lookup 24 microseconds per iteration
inline 1 microseconds per iteration
noop 0 microseconds per iteration

Apparently the interp alias method has been improved a bit, now being a viable alternative.

# The code
  namespace eval dad {
      proc test {x args} {set x}
      test 5
      namespace export *
  }
  namespace eval son {}


  proc t {n} {

      # 1. Plain proc call
      proc ::son::test {x args} {set x}
      ::son::test 5
      set t [time {::son::test 5} $n]
      namespace eval :: {rename ::son::test {}}
      puts "  direct           : $t"

      # 2. namespace import
      namespace eval ::son namespace import -force ::dad::*
      set t [time {::son::test 5} $n]
      puts "  import           : $t"

      # 3. interp alias
      interp alias {} ::son::test {} ::dad::test
      set t [time {::son::test 5} $n]
      interp alias {} ::son::test {}
      puts "  interp alias     : $t"

      # 4. indirect through proc
      proc ::son::test {x args} {eval [linsert $args 0 ::dad::test $x]}
      ::son::test 5
      set t [time {::son::test 5} $n]
      namespace eval :: {rename ::son::test {}}
      puts "  indirect         : $t"

      # 5. store in array (it's name is the empty string!)
      set ::(::son::test) ::dad::test
      set t [time {$::(::son::test) 5} $n]
      puts "  array            : $t"

      # 6. switch
      proc ::son {method args} {
        switch $method {
            a     {}
            b     {}
            test     {return [eval [linsert $args 0 ::dad::test]]}
        }
      }
      ::son test 5
      set t [time {::son  test 5} $n]
      namespace eval :: {rename ::son {}}
      puts "  switch           : $t"

      # 7. lookup
      set ::b [list ::dad ::none]
      proc ::son {method args} {
          foreach anc $::b {
              if {[llength [info proc ${anc}::$method]]} {
                  return [eval  [linsert $args 0 ${anc}::$method]]
              }
          }
      }
      ::son test 5
      set t [time {::son  test 5} $n]
      namespace eval :: {rename ::son {}}
      puts "  lookup           : $t"

      # 8. inline
      set x 5
      set t [time {set x} $n]
      puts "  inline           : $t"

      # 9. empty loop
      set x 5
      set t [time {} $n]
      puts "  noop             : $t"


  }


  t 30000