From: David E. Young
Subject: Portable MOP functionality?
Date: 
Message-ID: <6FxE9.145617$dn3.7814913@twister.southeast.rr.com>
Greetings. I've written a bit of code that hooks into the slot access
protocol of the MOP to monitor changes in an instance's slot values. In
brief, this involved writing an around method on shared-initialize that
binds a special variable, and an after method on (setf
slot-value-using-class) that does the actual work. [I'm doing this because I
need to distinguish between changes to instances made by *my* system (LISA)
and changes made outside the system, by user code.]

Using ACL 6.2, this works just fine. Using LispWorks 4.2, I've found that it
doesn't work if a slot is set via an accessor method rather than (setf
slot-value). After googling a bit, I found an LCL doc that indicates calls
to slot-value within methods can be open-coded by the compiler, and I
suspect this is the case with LispWorks. Could be wrong, though.

To test this theory, I attached just a reader method to a slot, then defined
a writer via DEFSETF that simply invokes (setf slot-value). This works fine
with LispWorks.

I've seen nothing in the CLOS/MOP documentation that indicates what
LispWorks does is contrary to the "rules", assuming I understand things,
which seems to imply my approach isn't portable. So, if that's true, then
what is the Right Way to do this sort of thing? Thanks much.

Regards,
--
------------------------------------------
David E. Young
·······@pobox.com
http://lisa.sourceforge.net

"One Ring to rule them all, One Ring to find them,
 One Ring to bring them all and in the darkness bind them."
  -- From an Elven-lore verse

"But all the world understands my language."
  -- Franz Joseph Haydn (1732-1809)

From: Brad Miller
Subject: Re: Portable MOP functionality?
Date: 
Message-ID: <arueg9$u01$1@localhost.localdomain>
"David E. Young" <·······@nc.rr.com> wrote in message
·····························@twister.southeast.rr.com...
>
> I've seen nothing in the CLOS/MOP documentation that indicates what
> LispWorks does is contrary to the "rules", assuming I understand things,
> which seems to imply my approach isn't portable. So, if that's true, then
> what is the Right Way to do this sort of thing? Thanks much.
>

Well, I won't argue it's "the right way" but I've given up on any
portability in MOP between different implementations. I write my own
defclass macro instead and work from there - I can then hook every accessor.
I also redefine slot-value to get hooks there too. Loads of fun!
From: Tim Bradshaw
Subject: Re: Portable MOP functionality?
Date: 
Message-ID: <ey3of8d6yli.fsf@cley.com>
* David E Young wrote:
> I've seen nothing in the CLOS/MOP documentation that indicates what
> LispWorks does is contrary to the "rules", assuming I understand things,
> which seems to imply my approach isn't portable. So, if that's true, then
> what is the Right Way to do this sort of thing? Thanks much.

It's good that it isn't contrary to the rules, I think.  Even when I
knew the AMOP MOP better I could never convince myself that really
AMOP-conforming implementations could avoid going through the whole
SLOT-VALUE-USING-CLASS thing, requiring GF dispatch on the thing you
most want to be fast in the whole system...

I hate to say it, but what I'd do in a case like this would be to have
a macro on top of DEFCLASS which grovelled out the writer names and
defined after methods on them to do the extra stuff.  You could
probably do this `more cleanly' with a metaclass by intervening in the
initialisation of the class object, and defining some after methods
on the writers.

This reminds me of something.  On Genera, accessor performance was
very good indeed in many cases - as fast as SLOT-VALUE and very close
to structure slot accessors.  It did this by clever tricks which
involved knowing about the accessors within methods I think.  However,
if you defined a non-primary method on an accessor, it would become
horribly slow, because it checked some `OK' bit, and then punted to
much worse code.  This could be fixed by declaring accessors
NOTINLINE, which stopped it optimising the good case, but made the bad
case the same as ordinary GF call (and the  same as the good case).

So, I was wondering: are there other implementations which have this
kind of issue?  Should I worry that declaring AROUND methods, say, on
accessors might have terrible performance costs on some system?  It
doesn't seem to on any systems I have access to, but ...

--tim
From: Kenny Tilton
Subject: Re: Portable MOP functionality?
Date: 
Message-ID: <3DE2B2E1.8070700@nyc.rr.com>
Tim Bradshaw wrote:
> * David E Young wrote:
> 
>>I've seen nothing in the CLOS/MOP documentation that indicates what
>>LispWorks does is contrary to the "rules", assuming I understand things,
>>which seems to imply my approach isn't portable. So, if that's true, then
>>what is the Right Way to do this sort of thing? Thanks much.
> 
> 
> It's good that it isn't contrary to the rules, I think.  Even when I
> knew the AMOP MOP better I could never convince myself that really
> AMOP-conforming implementations could avoid going through the whole
> SLOT-VALUE-USING-CLASS thing, requiring GF dispatch on the thing you
> most want to be fast in the whole system...

check. i meant to add to my post that my SVUCs whacked ACLs 
optimizations thereof, tot he tune of things running 30% slower. Of 
course CPUs got 30% faster while I typed this, but still, I hate 
throwing away cycles when the defclass macro serves as well.

-- 

  kenny tilton
  clinisys, inc
  ---------------------------------------------------------------
""Well, I've wrestled with reality for thirty-five years, Doctor,
   and I'm happy to state I finally won out over it.""
                                                   Elwood P. Dowd
From: Tim Bradshaw
Subject: Re: Portable MOP functionality?
Date: 
Message-ID: <ey37kf16x2r.fsf@cley.com>
* Kenny Tilton wrote:
> check. i meant to add to my post that my SVUCs whacked ACLs
> optimizations thereof, tot he tune of things running 30% slower. Of
> course CPUs got 30% faster while I typed this, but still, I hate
> throwing away cycles when the defclass macro serves as well.

30% is OK (well, for me).  The Genera slowdown I mentioned in some
related thread was factors of several, and a lot of consing.  That's a
lot worse...

--tim
From: Gerd Moellmann
Subject: Re: Portable MOP functionality?
Date: 
Message-ID: <86adjxrwby.fsf@gerd.free-bsd.org>
Tim Bradshaw <···@cley.com> writes:

> This reminds me of something.  On Genera, accessor performance was
> very good indeed in many cases - as fast as SLOT-VALUE and very close
> to structure slot accessors.  It did this by clever tricks which
> involved knowing about the accessors within methods I think.  However,
> if you defined a non-primary method on an accessor, it would become
> horribly slow, because it checked some `OK' bit, and then punted to
> much worse code.  This could be fixed by declaring accessors
> NOTINLINE, which stopped it optimising the good case, but made the bad
> case the same as ordinary GF call (and the  same as the good case).
> 
> So, I was wondering: are there other implementations which have this
> kind of issue?  Should I worry that declaring AROUND methods, say, on
> accessors might have terrible performance costs on some system?  It
> doesn't seem to on any systems I have access to, but ...

Don't know about the "terrible", but I can tell the story from PCL's
point of view.

I've recently added slot accessor gf call optimization to PCL, for
CMUCL, which means that in code like

  (defclass foo ()
    ((a :reader foo-a)))

  (defmethod bar ((x foo))
    (foo-a x))

the call to FOO-A can be optimized like SLOT-VALUE, provided PCL has
enough knowledge about class FOO at compile time.  (BTW, a paper about
CLOS optimization from Cyphes & Moon, Symbolics, 1990 mentions this
optimization, but says it's not (yet?) implemented.)

The resulting inline code looks like this

  (let* ((index <some-constant-computed-at-compile-time)
         (location (svref <a-permutation-vector> index))
         (value (typecase location
                  (fixnum (svref <the-instance's-slot-vector> location))
                  (cons   (cdr location))
                  (t      +slot-unbound+))))
     (if (eq value +slot-unbound+)
         (foo-a <the-instance>)
         value))

The FIXNUM and CONS cases correspond to instance and class slots.
Most of the <...> values are computed once at method entry, except
INDEX, which is constant.  I'll leave a lot of details out, but it
might be interesting that <a-permutation-vector> is the result of a
cache lookup that is done once at the start of the method for the
types of actual arguments, and that <a-permutation-vector> is usable
for all optimized SLOT-VALUEs, (SETF SLOT-VALUE)s, SLOT-BOUNDPs, slot
accessor calls (and the same cache lookup is also used for more
general gf call optimization, that I've also added to PCL).  So, the
amortized cost of one optimized access, when there are enough of them,
is pretty low, probably as low as it can get, without giving up
flexibility like being able to redefine classes.

The result of SLOT-VALUE optimization in methods looks the same,
except that the call to #'FOO-A is replaced with a SLOT-VALUE.

When a non-slot-accessor method is added, say an after method on
#'FOO-A, some slots in <a-permutation-vector> are set to NIL so that
the above code takes the T branch of the TYPECASE and a full gf call
is done, iff the after method must be run.


Where the above optimization is not applicable, for example outside of
methods, slot accessor calls are optimized differently.

Looking at the reader case only, a method function is generated that
contains internally the code snippet above, for SLOT-VALUE, plus the
cache machinery that is needed to make that code work.  One such
method function is generated for each slot name, somewhat like

  (defun slot-a-reader (x)
    <do the cache stuff>
    (let* ((location (svref <a-permutation-vector> 0))
           (value (typecase location
                    (fixnum (svref <x's-slot-vector> location))
                    (cons   (cdr location))
                    (t      +slot-unbound+))))
       (if (eq value +slot-unbound+)
           (slot-value x 'a)
           value)))

When there are only primary methods on #'FOO, these method functions,
which are effectively already effective methods, can be invoked with
pretty low cost (no cache lookup necessary to get at the effective
method).

Adding a non-primary method to #'FOO-A means that this last
optimization can't be done.  The call to #'FOO-A becomes slightly more
expensive, not considering the cost of calling the diverse method
functions in the effective method.

To come to an end, there is a considerable cost involved when adding
non-primary methods.  If it's terrible depends, as usual.
From: David E. Young
Subject: Re: Portable MOP functionality?
Date: 
Message-ID: <RHAE9.145992$dn3.7983311@twister.southeast.rr.com>
Ok. Well, I shouldn't be too surprised I guess. I think I prefer the
metaclass approach to my own DEFCLASS, but I suppose I'll have to consider
the impact on users of the system. With my current approach the requirement
for users wishing the "auto-update" functionality would be to inherit from a
LISA mixin class. A metaclass would mean, well, a metaclass rather than a
mixin. This is an optional feature, so if there's a performance issue with
some Lisps users can disable it.

Writing a metaclass is unexplored territory for me. Does anyone offhand have
a nominal list of generic functions I must implement when writing a new
metaclass? Thanks again...

-- dey

"Tim Bradshaw" <···@cley.com> wrote in message
····················@cley.com...
>
> I hate to say it, but what I'd do in a case like this would be to have
> a macro on top of DEFCLASS which grovelled out the writer names and
> defined after methods on them to do the extra stuff.  You could
> probably do this `more cleanly' with a metaclass by intervening in the
> initialisation of the class object, and defining some after methods
> on the writers...
From: Steven M. Haflich
Subject: Re: Portable MOP functionality?
Date: 
Message-ID: <aruntb$ivg$1@news.franz.com>
Tim Bradshaw wrote:

>>I've seen nothing in the CLOS/MOP documentation that indicates what
>>LispWorks does is contrary to the "rules", assuming I understand things,
>>which seems to imply my approach isn't portable. So, if that's true, then
>>what is the Right Way to do this sort of thing? Thanks much.
> 
> It's good that it isn't contrary to the rules, I think.  Even when I
> knew the AMOP MOP better I could never convince myself that really
> AMOP-conforming implementations could avoid going through the whole
> SLOT-VALUE-USING-CLASS thing, requiring GF dispatch on the thing you
> most want to be fast in the whole system...

Tim, I'm surprised that you can't figure this out.  Like with
most of CLOS you have to think out of the plane of the problem
to see how to bypass the full gf invocation of S-V-U-C etc.

The trick is that a class needs to keep track of whether any
additional S-V-U-C or (SETF SVUC) are applicable to it.  If
there are none, then the implementation can substitute its own
extremely optimized versions that have the same effect as the
default metaclasses.

This bookkeeping isn't trivial, of course, and it took a while
to get all the bugs (eql methods, specialization on
effective-slot-definition-class, etc. etc.) out of the Franz
implementation, but it's some years since any new bugs have
appeared.

With this bookkeeping in place, simple accessors can be
heavily optimized.

> This reminds me of something.  On Genera, accessor performance was
> very good indeed in many cases - as fast as SLOT-VALUE and very close
> to structure slot accessors.  It did this by clever tricks which
> involved knowing about the accessors within methods I think.

I think this is also fairly standard these days.
From: Tim Bradshaw
Subject: Re: Portable MOP functionality?
Date: 
Message-ID: <ey3y97g675s.fsf@cley.com>
* Steven M Haflich wrote:

> The trick is that a class needs to keep track of whether any
> additional S-V-U-C or (SETF SVUC) are applicable to it.  If
> there are none, then the implementation can substitute its own
> extremely optimized versions that have the same effect as the
> default metaclasses.

But, in order to do this, you need to be willing to go back and
unoptimize calls to SLOT-VALUE as far as I can see, since you can
never know whether there will be methods on SVUC.  Do people do that?

--tim
From: Steven M. Haflich
Subject: Re: Portable MOP functionality?
Date: 
Message-ID: <3DE44E81.9070102@alum.mit.edu>
Tim Bradshaw wrote:

>>The trick is that a class needs to keep track of whether any
>>additional S-V-U-C or (SETF SVUC) are applicable to it.  If
>>there are none, then the implementation can substitute its own
>>extremely optimized versions that have the same effect as the
>>default metaclasses.
> 
> But, in order to do this, you need to be willing to go back and
> unoptimize calls to SLOT-VALUE as far as I can see, since you can
> never know whether there will be methods on SVUC.  Do people do that?

Yup.  The most important thing is not to break the semantics.

I can't keep all the details straight in my head, but the gist
of it is this:

Discriminating functions cache effective methods for a call
to a gf.  If the df notices that the em for a particular call
is an uncustomized, simple slot reader or writer, then it can
instead cache the index of the slot in the instance and just
do the deed by itself rather than calling the em to do the
job.  Normal cache invalidation when the class is refinalized
handles the case of the instance structure changing.  The
implementation only needs also track when s-v-u-c methods
change.

As for reference to slots within a method, the class needs
to maintain a vector mapping slot offsets for each class
contributing methods to each finalized class containing that
class.  These optimize slot-value and are used if no
specialized behavior has been defined on any metaclasses.  If
specialized methods exist, the code falls back to s-v-u-c.
I think this happens at compile time, if the implementation
can tell, otherwise at run time.
From: Kenny Tilton
Subject: Re: Portable MOP functionality?
Date: 
Message-ID: <3DE2AEC9.50609@nyc.rr.com>
David E. Young wrote:
> I've seen nothing in the CLOS/MOP documentation that indicates what
> LispWorks does is contrary to the "rules", assuming I understand things,
> which seems to imply my approach isn't portable. So, if that's true, then
> what is the Right Way to do this sort of thing? Thanks much.

Well, my info is dated because it has been a while (several years) but 
for example MCL does not expose slot-value-using-class (SVUC). They 
shipped an ancient contrib documented as being for, what?, MCL2?. 
Anyway, the general answer is that SVUC is not in the spec, so 
portability gets tough. (I am surprised to hear about LW, btw, but I 
have to admit I have never heard anything specific about their MOP support.)

What I did when I went for universality with Cells was create a defclass 
wrappper which look for slots declared to be cellular and for those had 
the wrapper write accessors for those slots which called my own 
md-slot-value and (setf md-slot-value) stand-ins.

The bad news is that now slot-value and (setf slot-value) are back 
doors, but in my case that actually simplified my internals and, hell, 
I'll just document it. :)

-- 

  kenny tilton
  clinisys, inc
  ---------------------------------------------------------------
""Well, I've wrestled with reality for thirty-five years, Doctor,
   and I'm happy to state I finally won out over it.""
                                                   Elwood P. Dowd