Once upon a time, I wrote two blog entries about precompiling the dispatch for generic functions in order to improve the responsiveness at application startup, and obviously planned a third but didn't get around to writing it. Some time later, I asked the lazyweb about it, and unfortunately this time didn't get anything much back. On further reflection, I think I was going to discuss a couple of additional technical points.
First of all, there's the question about having this kind of precompilation on all the time; wouldn't it be nice if generic functions were always at their most efficient, ready to be called? Well, yes, but what we actually want is for functions to be at their most efficient when we want to call them, and we don't particularly care at other times. Why draw that distinction? Well, when we write code that uses generic functions, it is usually of the form:
(defgeneric foo (x y))
(defmethod foo ((x t) (y t)) ...)
(defmethod foo ((x integer) (y integer)) ...)
(defmethod foo ((x rational) (y float)) ...)
a sequence of method definitions; sometimes contiguous, as above;
often distributed among various files.
load
ing
such code will execute the
defmethod
forms in series. And each of those executions will call
add-method
,
changing the generic function's methods, so any aggressive dispatch
precompilation scheme will kick in at every defmethod
. That would
be bad enough, being O(N) in the number of methods, but it's
actually worse than that: the amount of work involved in
precompilation will tend to be O(N) in the number of methods, or
in the number of concrete classes applicable to those methods, so
there's O(N2) or worse behaviour from having
precompilation active all the time. SBCL is
routinely mocked for having slow-loading FASL files (originally, FASL
probably stood for FASt Load; we don't advertise that much); expensive
and poorly-scaling computations at load-time probably won't win us any
extra friends. (Though having written that, I now wonder if recording
changed generic functions using the dependent-update protocol, and
hooking load
or asdf:load-system
to precompile after loading a
whole file or system, might be a tasteful compromise).
Secondly, there's the detail of exactly how to pre-fill the cache for a given generic function (given that its specializers are mostly or entirely classes). The simplest strategy, of filling the cache with exactly those classes used as specializers, fails as soon as abstract or protocol classes are used to organize the implementation of the generic function. The next simplest, which is to fill the cache with all possible subclasses of specializers at all argument positions, well, just reading that should set alarm bells ringing – it might not be too problematic for generic functions with one argument, but the cross-product effect of multiple arguments will probably cause the implementation to collapse under its own weight into a virtual black hole. It might be that the best approach is to allow the user to specify an exact set of classes: and to allow the user to introspect a running system to find out which class combinations have actually been seen and hence cached in a given session.
In fact, SBCL itself depends on a particular, specific version of
this. The generic function
print-object
is used for printing almost everything; even without user
specialization, it is called whenever a
structure-object
or
standard-object
needs to be output to any stream. Users can write methods on it,
though, and that then invalidates any previous precomputed dispatch
information. But there are times when it's really important that
print-object
just work, without any kind of extra computation to go
on: in particular, when the debugger needs to inform the user that the
Lisp is running out of storage space (heap or stack), it's pretty
important that that reporting can happen without using more heap or
stack than necessary. So for print-object
, we never reset the cache
state to empty; it is always prefilled with entries for
control-stack-exhausted
, binding-stack-exhausted
,
alien-stack-exhausted
, heap-exhausted-error
and
restart
for its first argument. (This strategy only works if there are no
specializations of the second argument to print-object
anywhere in
the system; there's no good reason for a user to try to specialize the
second argument in any case, so we emit a warning if they do that and
hope that they read and learn from it.)