From: cyphers@odi.COM
Subject: Attempting to circumvent Symbolics' Reader Method optimization
Organization: The Ohio State University Department of Computer and Information Science
Date: Wed, 8 Apr 1992 19:40:23 GMT

I don't have sources, CLOS specs, or Ivory specs to look at, so I'm
doing this all from memory; names are only approximate.

   Date: Wed, 8 Apr 1992 11:59-0400
   From: Scott McKay <SWM@SAPSUCKER.SCRC.Symbolics.COM>

       Date: Tue, 7 Apr 1992 11:38 EDT
       From: basham@aio.jsc.nasa.gov (Bryan Basham)

       Hello SLUG,

       I have written a piece of code that dependends on the reader methods to call
       SLOT-VALUE.  However, I have discovered that Symbolics' MOP performs an
       optimization (?) that does not call SLOT-VALUE.  I would like to circumvent
       the "empty" creation of Reader Methods and place a real method (that calls
       SLOT-VALUE in its place).

   Genera CLOS optimizes slot accesses (via WITH-SLOTS, SLOT-VALUE, and
   reader and writer methods) to use the slot read/write instructions for
   all classes whose metaclass is STANDARD-CLASS.  This results in a
   substantial performance improvement (*very* substantial).

   You can disable the optimization for readers and writers by proclaiming
   them to be NOTINLINE.  I don't think you can disable the optimization
   for SLOT-VALUE and WITH-SLOTS.

It sounds like a bug if declaring slot-value or (setf slot-value)
notinline doesn't stop the optimization.  It also sounds like a bug if
you have specialized slot-value-using-class for your class (you can do
it with an eql specializer on your class) and your method isn't
getting called.  But, as SWM says, you would be far better off using a
metaclass of your own than trying to mess with what happens with
standard-class.  Most of the metaclasses are instances of
standard-class; you can quickly put yourself in a pretty bad state
where nothing works.

   I think what you probably want to do is to (1) create a new metaclass
   that inherits from STANDARD-CLASS, (2) specialize SLOT-VALUE-USING-CLASS
   and (SETF SLOT-VALUE-USING-CLASS) for that class, and (3) make all of
   your classes use the new metaclass by supplying the :METACLASS option to
   DEFCLASS.

       In the past week I have reviewed the following code:

	 1) for class creation
	    -- This lead me to the realization that although reader methods are created
	    they are not used by the reader generic function.  Instead, slot access is
	    handled by the function CLOS-INTERNALS::STANDARD-CLASS-SHARED-SLOT-READER
	    (and is dispatched by: CLOS-INTERNALS::HANDLE-MISSED-DISPATCH).

Almost every generic dispatch goes through handle-missed-dispatch the first time
a particular argument signature is seen.  handle-missed-dispatch computes the combined
method and sticks it in a cache, so that the next time you go directly there.

	 2) for reader method creation and insertion
	    -- Here, I discovered the generic function COMPUTE-SLOT-METHODS, which is
	    called by the SHARED-INITIALIZE (STANDARD-CLASS) after method, creates
	    instances of STANDARD-READER-METHOD and adds the methods to the proper
	    reader generic function, using ADD-METHOD.  This lead me to think that I
	    could write an after method for ADD-METHOD that would insert the proper
	    functional code.

There are these functions with names like make-reader-method or something like that
which would be the right place to add your hooks.  They're probably in the basic-methods
file.

	 3) for method creation
	    -- Here I discovered just how convoluted Symbolics' generic function/method
	    implementation is.  The magical incantations that associate a real function
	    to a method and then the method to a generic function is distributed over
	    several files.

Compiling/loading CLOS into a CLOS-less environment has a lot to do
with what files various things end up in.  It's also possible to do
things like redefine standard-class with an extra slot, which is nice
for things like patching and impressing people.

	 4) for generic function dispatching
	    -- I'm at a loss to figure out what's going on here.

%GENERIC-DISPATCH does a hash lookup of what's in FP|2 in a table
indicated by FP|3.  If the lookup succeeds, FP|2 is replaced by
something in the table and a jump is performed to a PC in the table.
Otherwise, FP|2 is left as is, and a default PC from the table is
jumped to.  %MESSAGE-DISPATCH uses FP|3 as the key and gets the table
from FP|2, but is otherwise like %GENERIC-DISPATCH, except that what
started in FP|2 gets moved to FP|3 in the process.  %GENERIC-DISPATCH
is used for a class dispatch, and %MESSAGE-DISPATCH is used for an EQL
dispatch (actually an EQ dispatch.  When EQ and EQL aren't the same,
like for a bignum, the dispatch is done by an assembled helper).  I
think this is explained in a comment someplace, like the yad file.
yad stands for "yet another dispatcher".  The first two were even more
impossible to understand.  It is important to know that dispatching is
done as a series of jumps from helper to helper, and the helpers
aren't functions.

The handler for a generic function with a particular argument
signature takes one extra argument, called the extra argument, all
over the place.  The handlers are all parameterized, and this extra
argument is the parameter.  Sometimes it's an array, sometimes a list,
sometimes a number.  All the stuff that makes up a combined method
expects to see a function and an extra argument value.

There is also some support (see basic-methods and mapping-tables) to
allow an optimized slot value to invoke an arbitrary function.  I
think this is how I did some of the exception handling stuff.

The way the push-slot-value instruction works is that FP|2, the extra
argument, is supposed to have a vector in it (called a mapping table),
and FP|3 is supposed to have an instance.  The microcode checks these
assumptions, adds the immediate argument of the instruction to the
mapping table and reads the value at that location.  If it's a fixnum,
it's added to the address of the instance and the contents of that
location are pushed.  A lot of those things happen in parallel with
each other, and there's also some cacheing to make it even faster.
Anyways, if all the assumptions aren't met, a trap handler is invoked.
It's in something like sys:debugger;itrap, which you might not have
the source for.  Assuming the trap happenned because there wasn't a
fixnum in the mapping table at the indicate offset, the type of the
value that was in the table determines what will happen.  A NIL means
the mapping table is obsolete (the instance's class was redefined after
the generic dispatch had determined an extra argument).  In that case, the
proper mapping table is gotten and we try again.  A locative in the table
means the contents of the locative should be used -- this is how shared
slots work.  If there's a function in the table, the function is called
with arguments which tell what kind of access (slot-value, (setf slot-value),
etc.) and things like that. For example, for an optimized call to a reader,
where the reader needed to do more than slot-value, the function would call
the reader with appropriate arguments.


There is also a short paper which David Moon and I wrote for the CLOS
workshop at OOPSLA in '90 which gives an overview of all this stuff.
Symbolics can probably get you a copy of the file.
Newsgroups: comp.lang.clos