Previous Table of Contents Next


The idea is simple: Set the slot value in the second object to any value (e.g., ‘unknown) at the same time the value in the third object is changed. Changing the slot value in the second object triggers the after method to make available the update function to the first object. Thereafter, the value in the first object is subject to recalculation as desired. If you do access the value in the first object, it accesses the value in the second object for its recalculation, which is also recalculated on demand. Here’s the new version of add-dependency, which does the job:

   (defun add-dependency (this-object this-slot update-fcn
                    &rest objects-and-slots)
      (do* ((xx objects-and-slots (cddr xx))
       (newobj (car xx) (car xx))
       (newslot (cadr xx) (cadr xx)))
         ((null xx))
       (add-update this-object this-slot update-fcn)
       (setf (getf (part-dependency newobj) newslot)
        #’(lambda ()
            (let ((fcn (getf (part-updates this-object)
                         this-slot))
              (fcn1 (getf (part-unused-updates this-object)
                       this-slot))
              (unless fcn
                (setf (slot-value this-object this-slot)
                     ‘unknown))
                   (setf

                     (getf (part-updates this-object) this-slot)
                     fcn1)))))))

The change is in the lambda expression—that is, the function that gets run whenever (setf slot-value) is called on a slot that this-slot of this-object depends on. It checks to see if there is an update function that needs to be run when this-slot is read from. If there is, then it returns because the slot is out of date (i.e., someone else already installed the update function for the slot). Otherwise, it setfs the slot value to unknown (thus recursively notifying slots and objects that are dependent on this value) and then takes the update function out of the updates-unused association list and puts it back in the association list of the updates slot of this-object, where it is found and run next time this-slot is accessed.

This solution ignores many issues that robust code would have to deal with. More sophisticated implementations might want to set multiple dependencies on a slot of an object.

More customization might also be needed for dependency-class: For example, you would want to deal with the possibility of the slots being unbound. The following code indicates useful MOP generic functions:

   (defgeneric slot-unboundp-using-class (class instance slot-name)
    ;; generic function for testing if a slot has a value
    )

   (defgeneric slot-makunbound-using-class (class instance slot-name)
    ;; generic function to make a slot have no value
    )

5.4.10. Performance Considerations

In general, the default CLOS operations are highly optimized. Generic functions dispatch using caching techniques.

Build it right and get it working before you speed up anything. When you have to speed it up, profile your code! Profiling is really the only way to ensure you aren’t wasting your time fixing things that matter little.

Here are a few hints for increasing speed when it is really necessary:

  Some implementations offer specialized CLOS semantics that are more restrictive than the default CLOS but that the compiler can use to optimize access. For example, a popular implementation, Allegro CL, offers a fixed-index metaclass that implements slot value as array memory accesses.
  If speed is really essential, consider recoding the performance critical section in straight procedural (non-CLOS) Common Lisp. This may add speed.
  Most implementations generate dispatching code for calculating which method to use based on the generic function. Because there is a large number of possibilities for different methods, many implementations generate the dispatching code at runtime. This needs to be compiled to run fast, introducing a delay the first time the method is invoked. Using the application before delivery precalculates the dispatching code.
  Many implementations have special cases for the dispatching code for common patterns of method arguments. For example, most CLOS operators dispatch only on the first argument and have fewer than 10 arguments overall. Of the remainder, most of the rest dispatch on either the second argument or on both the first and second arguments. Specialized methods that conform to these cases are likely to be faster.
  Never use the MOP to customize standard-class directly. Always subclass it first. In the presence of customizations, the compiler may not be able to optimize as efficiently.

For a more general book about improving performance in Common Lisp applications, you should look at Paradigms of AI Programming (Norvig, 1992), which has an excellent section on optimizing general Common Lisp programs.


Previous Table of Contents Next