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 13:17, Tim Bradshaw <[email protected]> wrote:

> I'm now feeling my oats and have been thinking about a more general thing where you'd define a 'structure' with slots of various types and it will write the accessors for dealing with the several underlying arrays.

It would be an efficient way but using SoAs in Lisp with multiple arrays still may put you to some disadvantage compared to lower level languages that support type casting, in that you will keep jumping between memory locations of arrays of various types. It will be certainly faster than using plain CLOS objects, or structures, but you will be constantly changing the memory locations making the prefetcher’s head spin. Have a look at this graphic for an alignment for a theoretical struct that has 2 float fields, 3 doubles, and 4 bytes:

STRUCT OF ARRAYS (SoA) CONTAINER
     +-----------------------------------------------------------------------+
     |                                                                       |
     |   Index (i) ->      0          1          2          3         ...    |
     |                  +-----+    +-----+    +-----+    +-----+             |
     |   float  f1[]:   | 1.0 |    | 2.5 |    | 3.1 |    | 4.2 |      ...    |
     |                  +-----+    +-----+    +-----+    +-----+             |
     |   float  f2[]:   | 0.5 |    | 0.1 |    | 0.9 |    | 0.7 |      ...    |
     |                  +-----+    +-----+    +-----+    +-----+             |
     |                                                                       |
     |                  +-----+    +-----+    +-----+    +-----+             |
     |   double d1[]:   | 3.14|    | 6.28|    | 9.42|    |12.56|      ...    |
     |                  +-----+    +-----+    +-----+    +-----+             |
     |   double d2[]:   | 1.11|    | 2.22|    | 3.33|    | 4.44|      ...    |
     |                  +-----+    +-----+    +-----+    +-----+             |
     |   double d3[]:   | 9.99|    | 8.88|    | 7.77|    | 6.66|      ...    |
     |                  +-----+    +-----+    +-----+    +-----+             |
     |                                                                       |
     |                  +--+       +--+       +--+       +--+                |
     |   byte   b1[]:   |01|       |00|       |FF|       |A1|         ...    |
     |                  +--+       +--+       +--+       +--+                |
     |   byte   b2[]:   |02|       |00|       |EE|       |B2|         ...    |
     |                  +--+       +--+       +--+       +--+                |
     |   byte   b3[]:   |03|       |00|       |DD|       |C3|         ...    |
     |                  +--+       +--+       +--+       +--+                |
     |   byte   b4[]:   |04|       |00|       |CC|       |D4|         ...    |
     |                  +--+       +--+       +--+       +--+                |
     |                                                                       |
     +-----------------------------------------------------------------------+
                         ^
                         |
      To access "Object 0",
      you read index 0 from
      all 9 arrays.

The advantage of this layout is that the same fields are close together, making it easy to put them through the SIMD registers in a single load/unload operation. However reading struct fields individually will force you to move to a different array's memory location every time, which could be potentially quite far, depending on how many structs you are working with and it is going to affect performance negatively. If this (reading individual struct fields) is what you need, then AoS from statically typed languages may be more preferable here, have a look at this diagram:

SINGLE CONTIGUOUS MEMORY BLOCK (Vector<byte> or void*)

    Base Pointer
    |
    v
    +---------------------------------------------------------------+
    |                  SECTION 1: DOUBLES (Alignment 8)             |
    |  [ d1 array (n) ]  |  [ d2 array (n) ]  |  [ d3 array (n) ]   |
    +---------------------------------------------------------------+
    |                                                               |
    |  OFFSET: 0         OFFSET: 8*n          OFFSET: 16*n          |
    |                                                               |
    +---------------------------------------------------------------+
    |                  SECTION 2: FLOATS  (Alignment 4)             |
    |         [ f1 array (n) ]      |      [ f2 array (n) ]         |
    +---------------------------------------------------------------+
    |                                                               |
    |         OFFSET: 24*n                 OFFSET: 28*n             |
    |                                                               |
    +---------------------------------------------------------------+
    |                  SECTION 3: BYTES   (Alignment 1)             |
    |   [b1]   |   [b2]   |   [b3]   |   [b4]   |                   |
    +---------------------------------------------------------------+
                                                ^
              OFFSET: 32*n   33*n   34*n   35*n |
                                                |
                                           End of Struct


Common Lisp can’t do such things due to limited numeric type casting support in the language, but you could try that in LW with the typed aref approach, it should work the same. This is essentially what a cast of a byte array to an array of structs looks like. 

You could also tweak the layout further to make it more convenient for SIMD register loading and unloading (by grouping the same fields of adjacent structs together), but that won’t be of much use in a lisp that does not support vectorisation. The layout will look like so, for those who are curious still:

ZOOMING IN ON "SECTION 1" (Offsets 0 to 8*N)
    +-----------------------------------------------------------------------+
    |  Value:   | d1[0]  | d1[1]  | d1[2]  | d1[3]  | ... | d1[N-1] |       |
    |  Entity:  | Ent 0  | Ent 1  | Ent 2  | Ent 3  | ... | Ent N-1 |       |
    |  Address: | 0x00   | 0x08   | 0x10   | 0x18   | ... | 8*(N-1) |       |
    +-----------------------------------------------------------------------+
                ^-----------------------------------^
                |
       CPU loads these 4 doubles into
       ONE AVX register (YMM) instantly.
       NO JUMPING.

d1[0], d1[1], d1[2], d1[3] … are just four double field belonging to four different structs located back to back. You sweep that into a 256 bits wide SIMD register at once, do an operation on them in a single go, and push it back as quickly. Repeat as necessary. Not possible in lisps now, but will be eventually.

> 
> In an implementation which can store unboxed doubles in structure/instance slots (single floats are likely immediate in any 64-bit implementation and are in LW, so doubles are the interesting case really) then array-of-structs still means that for every element you follow a pointer, likely check the type of the object there, and then have access to its unboxed fields.  For an implementation which can't do that you do all that and in addition have to follow a pointer for each field access, and almost certainly cons a double for each store which is horrid.  SOA means none of this happens.

Yep. It is not really a problem of lisp specifically, a lot of pointer chasing is necessary in other languages too when using reference class types, in lisp you do also have the boxing issues on top of that, but in short, AoS would be a less efficient approach to access data regardless of the language choice and would only be suitable for non-computational workloads, like a banking application, or in web development context and the likes.
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.