[gambit-list] Performance observations

Christian christian at pflanze.mine.nu
Fri Apr 28 12:19:00 EDT 2006


Hello

Here are a few observations about runtime performance (of compiled
code on Linux/x86).

- the generational garbage collector is about 3 times faster than
allocation/collection of "still" objects. But it looks like it is only
about as fast as the BoehmGC as being used by Bigloo. Comparing
Chicken with Gambit, it looks like Chicken allocates for all function
calls, but it has a collector which seems to be 3 times faster than
the generational collector of Gambit (Chicken is sometimes faster than
Gambit for allocation intensive code (like closures are about as cheap
as normal function calls), but only in benchmarking mode (-Ob), Gambit
is faster for most other code and less dependent on unsafe
optimizations.

It surprises me a bit that Chicken's stack allocation/scanning
approach is so fast, since also return addresses from function calls
are saved on the stack for no benefit (which should make the memory
usage slightly bigger and scanning more costly, right?). Maybe this is
highly cpu cache dependent?  Chicken does some timings during
configuration, which makes me suspect that there is room for tuning
for Gambit, too.

- function calls across compilation block boundaries usually are quite
slow (like hundreds of cpu cycles), I guess since they are looked up
dynamically by name on each call.

For example replacing calls to ##promise? with calls to my_promise?
with the definition
(define (my_promise? obj)
  (and (##subtyped? obj)
       (##eq? (##subtype obj)
	      12 ;; current definition of (macro-subtype-promise)
	      )))
in the same block makes the time needed for calling this predicate
drop from around 200 cpu cycles down to about 30 cycles.

A mechanism for static function calls across block boundaries might be
beneficial in such cases where a function is known not to change
during runtime except when a new object file is loaded. (An
alternative may be to include copies of those function definitions
which are speed critical into every block which is using them, that
also opens the possibility for inlining, but code bloat may be the
downside.)

(In case gambit is currently using a name lookup in a table for each
call: I'm wondering if (just an idea) using a box around the lambda
value wouldn't speed such calls up already. Callers could keep a
reference to the box and wouldn't have to do a costly table lookup
anymore on subsequent calls, and the system could still replace
functions at any time.)

Or are those cpu cache effects too?

- maybe as a result of the above, delay and force might be faster than
they are: a stream using those (in the same block) instead of the
builtin delay / force is about 1.5 times faster for me:

;; I couldn't find a more lowlevel way to allocate a promise
;; structure:
(define alloc_promise ##make-promise) 

(define-macro (my_delay body)
  `(alloc_promise (lambda ()
		    ,body)))

;; my_promise? -> see definition above

(define (my_force promise)
  (if (my_promise? promise)
      (let ((maybe-thunk (##vector-ref promise 0)))
	(if maybe-thunk
	    (let ((value (maybe-thunk)))
	      (##vector-set! promise 1 value)
	      (##vector-set! promise 0 #f)
	      value)
	    (##vector-ref promise 1)))
      (error "not a promise: " promise)))



Thanks
Christian.




More information about the Gambit-list mailing list