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.