TurtleWare<br>A brief note about slot access cost in Common Lisp
Tagged as lisp<br>Written on 2026-05-27 by Daniel Kochmański<br>Common Lisp is renowned for its excellent object system CLOS. Its implementation<br>is often accompanied by the Metaobject Protocol that, while it is not part of<br>the standard, allows programmers to customize the system underpinnings in<br>numerous interesting ways. This level of customization doesn't come without a<br>cost – some CLOS code paths will be slower compared to open-coding equivalent<br>solution while abstaining from using standard objects.
The purpose of this blog post is to draw an intuition of differences between<br>structure objects and standard objects when it comes to accessing their slots.<br>From now on I'm going to refer to structure objects as structures, and standard<br>objects as instances.
We could imagine a structure is represented in memory as a tuple (CLASS SLOTS),<br>while an instance is represented as a tuple (CLASS STAMP SLOTS). Modifying the<br>structure class has undefined behavior, while the instance's class may<br>change. This is why the instance needs to track whether it is up-to-date or<br>obsolete. In our simple scheme that information is represented by a stamp that<br>represents the class generation.
Tracking whether the instance is obsolete is important, because the memory<br>layout of slots may change - they may be deleted, added, or moved to different<br>positions. This is convenient for long-running programs without downtime, for<br>incremental development and for image-based workflows - the program may be<br>modified at any time to account for changing requirements, without recompiling<br>it from scratch.
But it doesn't come without a downside. The implementation may conformingly<br>assume, that structure accessors won't ever change and they may be inlined.<br>That means in particular, that structure access is a simple memory reference.
(declaim (inline structure-reader-a))<br>(defun structure-reader-a (object)<br>(svref (%slots object) 3))
While with the object we can't assume that, because we need to check whether the<br>object is not obsolete (at the very least), and because readers are more generic<br>functions - another level of flexibility. Inlining generic functions is hard,<br>because new methods may be added at runtime and the effective method can change.<br>Moreover there may be different classes that have same reader names, so we need<br>to include a piece of code that uses the correct class layout for an instance.
This is why calling instance readers involves:
calling a function (can't be inlined)
finding the memory layout (dispatch)
verifying whether the instance is up-to-date
That is exemplified by the following pseudocode that ignores other generic<br>function intrinsics. Depending on the implementation of generic functions, the<br>test for obsolete instances may be evaded when instances are not obsolete.
(declaim (notinline instance-reader-a))<br>(define-reader-function instance-reader-a (object)<br>(unless (%up-to-date-p object)<br>;; Among other things updates indexes for memory accesses.<br>;; This is a slow path.<br>(%recompile-reader-function #'instance-reader-a)<br>(return-from instance-reader-a (instance-reader-a object)))<br>(typecase object<br>(standard-class-a (svref (%slots object) 3))<br>(standard-class-b (svref (%slots object) 4))<br>(custom-class-c (slot-value object 'a))<br>(custom-class-d (slot-value object 'a))<br>(otherwise (no-applicable-method #'instance-reader-a object))))
All this is assuming, that we deal with standard readers. Using the metaobject<br>protocol it is possible to store slot values anywhere, most notably not in a<br>vector bundled with the instance, or to add additional preprocessing. I'm not<br>going to touch here much on MOP - it is just to signify, that standard readers<br>for standard classes may directly access the slot vector.
At minimum, assuming a single reader and a clever dispatch algorithm:
(declaim (notinline instance-reader-a))<br>(define-reader-function instance-reader-a (object)<br>(if (eql (stamp object) 42)<br>(svref (%slots object) 3)<br>(if (%up-to-date-p object)<br>(no-applicable-method #'instance-reader-a object)<br>(progn<br>(%recompile-reader-function #'instance-reader-a)<br>(return-from instance-reader-a (instance-reader-a object))))))
In other words comparing structure access with instance readers is comparing<br>apples to oranges, because the former is a memory access, while the latter is a<br>function call.
SLOT-VALUE will be even slower, because this function is a trampoline to a more<br>involved SLOT-VALUE-USING-CLASS, and to do that we need to:
read the object class
find the slot definition in the class
invoke a generic function SLOT-VALUE-USING-CLASS
The generic function SLOT-VALUE-USING-CLASS may be similar to the reader defined<br>above with a caveat, that it has more arguments to dispatch on (so the dispatch<br>procedure may be more involved). In any case, it is at least as slow as the<br>optimal reader defined above (a single reader for the standard class).
(defun slot-value (object slot-name)<br>(let* ((class...