Re: unboxed structure fields (or class slots)
"Yuri Davidovsky (as work at disclosure dot ie)" <[email protected]>
| Newsgroups | gmane.lisp.lispworks.general |
|---|---|
| Message-ID | <[email protected]> |
> On 7 Jan 2026, at 21:14, Martin Simmons <[email protected]> wrote: > > No, I think that will not work because with-slots just causes uses of slot-a > etc to call slot-value. I.e. accessing the underlying slot each time, not > some lexical variable that can hold the unboxed value. Well, in that case we could make a very good showcase for our beginner friends on the list who still may be struggling to figure out what is all the fuss about lisp. This is just a programming language, right? In short, implementing language features like they were there from the very start is one of the biggest selling points of lisps. What does one do when discovering that a certain feature not present in their language of choice? Typically one would just sigh and move on, hoping that some day, after half a decade or so, the feature will finally make it into the language (or maybe not). That is how it works in lisp. If the language does not have the feature, you just add it. The very reason why there were so many incompatible lisps at one stage is exactly that — it is impossible to contain them as users add their own functionality to the language until it diverges, losing compatibility with other lisps completely in process. Here we have an example of a commonly used object slot accessor macro, which creates shorthands for accessing the props of a class instance. The issue with accessing object fields that we have in lisp is that each access can carry a substantial overhead compared to lower level languages. Not an issue for business oriented applications, but it certainly is for software that needs to churn through a lot of numerical data as fast as possible (physics modelling, for example). So when we have something like this in our code (with-slots (slot-a slot-b slot-c) object :body) it means that on each slot access (like (list slot-a slot-b slot-c)) the object will be touched every time, as Martin has explained. So it turns out that for max performance we would need a slot accessor helper that minimises interaction with the instance to just a single read at the start, and a single update at the end of the code body, while the access within the body of code will be essentially “free”. Common Lisp does not provide such functionality, but it is actually very simple to implement it ourselves (let’s call it with-cached-slots): (defmacro with-cached-slots ((&rest slots) object &body body) (let ((g-object (gensym "OBJECT"))) `(let((,g-object ,object)) (let ,(loop for slot in slots collect `(,slot (slot-value ,g-object ',slot))) (prog1 (progn ,@body) ,@(loop for slot in slots collect `(setf (slot-value 'g-object ',slot) ,slot))))))) (editor::setup-indent "with-cached-slots" 2) This may look very hairy for those unfamiliar with moderately complex macro writing, but it is actually very simple to figure out how it works by looking at the macro's expansion: (with-cached-slots (slot-a slot-b slot-c) object (list slot-a slot-b slot-c)) It’s output will look as follows: (LET ((#:OBJECT42540 OBJECT)) (LET ((SLOT-A (SLOT-VALUE #:OBJECT42540 'SLOT-A)) (SLOT-B (SLOT-VALUE #:OBJECT42540 'SLOT-B)) (SLOT-C (SLOT-VALUE #:OBJECT42540 'SLOT-C))) (PROG1 (PROGN (LIST SLOT-A SLOT-B SLOT-C)) (SETF (SLOT-VALUE 'G-OBJECT 'SLOT-A) SLOT-A) (SETF (SLOT-VALUE 'G-OBJECT 'SLOT-B) SLOT-B) (SETF (SLOT-VALUE 'G-OBJECT 'SLOT-C) SLOT-C)))) Here we can see something simple, there is a series of steps: 1. We create a lexical scope that will hold the cached object instance slot values. 2. The body of code gets evaluated referencing the cached aliases, making slot value access very quick. 3. At the end of the code body the object slots get updated before the lexical scope terminates. 4. The prog1 bit ensures that we get the result from the body evaluation, not from the last setf line in the macro output. Here we will just get a list of the slot values accessed as the result. The example is intentionally simplistic to illustrate the principle, and since it is read-only, we do not really need the last slot values update part, making it actually slower than it has to be, but in a body code where a lot of slot value reads and updates are done, this should actually speed up things substantially. The trick about lexical scoping is that bindings in a LET form should essentially be the CPU register aliases (more or less). So instead of slow and laborious reading and writing of object fields, each taking at least several CPU cycles, we can instantly access and update those while they remain in the registers, only worrying about updating the object instance at the very end, when we are done. While semifunctional, the example has weak sides. Namely, oftentimes we just need to read values, but not update them, so the macro ideally should check and update only those slots that were updated within the cached lexical scope. That will be somewhat complex, but an easy solution would be to just have a macro specifically aimed to deal with read only slots, something like with-cached-readonly-slots, or something similar. If the slots need to be updated, then the full-fat version should be used, or we could extend the macro to also take a list of fields that will need to be updated at the end of the processing, something like (with-cached-slots (slot-a slot-b slot-c) ; read fields (slot-b slot-c) ; update fields object :body) Another thing is that we need to declare the slot types for top performance (to avoid tagging/boxing overhead). Simply adding a declaration like so (with-cached-slots (slot-a slot-b slot-c) object (declare (type double-float slot-a slot-b slot-c)) :body) should do the trick just fine (I think, the issue could be that we will be placing it within the prog1 form that creates its own lexical scope) but to make it less verbose and easy to edit, we could change our macro to take input similar to (with-cached-slots ((:fixnum slot-a) (:double slot-b slot-c)) object :body) and generate the declarations automatically and place them right after the initial slot values bindings, which would be a great homework exercise for anyone willing to improve their Common Lisp chops. But either way, this little detour should give a person starting on their Lisp journey a good idea what the concept of an extensible language is about.