7. Structures and Performance
As we saw last, classes in Moonli (and Common Lisp) are very dynamic. A lot many things take place at run-time. Unfortunately, this also incurs a run-time cost.
In some cases, you may not need all that dynamicity, but may instead need better performance. This is achieved through structures.
A structure is a simple container for data. They have fixed fields and no multiple inheritance. Structure of instances of classes with metaclass structure-class. For example, the below defines an instance point of structure-class.
If you are using the Moonli REPL for trying out the code in this tutorial series, you should start the REPL with
--enable-debuggeroption for this tutorial. You can also do this by starting the REPL the usual way and then callingcl-repl:enable-debugger().
defstruct point:
x = 0;
y = 0;
end
This defines:
- a constructor
make-point(:x, …, :y, …) - accessors
point-x(obj)andpoint-y(obj) - a predicate
point-p - a printed representation
Creating a structure instance:
defparameter *point* = make-point(:x, 3, :y, 4)
Accessing fields:
point-x(*point*)
#=> 3
point-y(*point*)
#=> 4
point-p(*point*)
#=> t
Redefining structures
There’s no standard way to redefine structures. If you redefine point class to include a third slot for z, the existing instances as well as the code associated with the older (before update) class may behave unpredictably. This is quite unlike the classes that we discussed in the last chapter.
defstruct point:
x = 0;
y = 0;
z = 0;
end
In fact, if you are using the Moonli REPL in its default settings, you will simply get an error message if you try to run the new definition of point:
warning: change in instance length of class point:
current length: 3
new length: 4
simple-error: attempt to redefine the structure-object class point incompatibly
with the current definition
Backtrace for: #<SB-THREAD:THREAD tid=259 "main thread" RUNNING {70054D05F3}>
0: ((LAMBDA NIL :IN UIOP/IMAGE:PRINT-BACKTRACE))
1: ((FLET "THUNK" :IN UIOP/STREAM:CALL-WITH-SAFE-IO-SYNTAX))
2: (SB-IMPL::%WITH-STANDARD-IO-SYNTAX #<FUNCTION (FLET "THUNK" :IN UIOP/STREAM:CALL-WITH-SAFE-IO-SYNTAX) {1014D13BB}>)
...
On the other hand, if you have enabled the debugger either by running cl-repl:enable-debugger() or by starting Moonli REPL with --enable-debugger, you will be dropped into the debugger:
warning: change in instance length of class point:
current length: 3
new length: 4
attempt to redefine the structure-object class point incompatibly with the
current definition
[Condition of type simple-error]
Restarts:
0: [continue] Use the new definition of point, invalidating already-loaded
code and instances.
1: [recklessly-continue] Use the new definition of point as if it were
compatible, allowing old accessors to use new
instances and allowing new accessors to use old
instances.
...
Backtrace:
0: (sb-kernel::%redefine-defstruct #<sb-kernel:structure-classoid point> #<sb-kernel:layout (ID=376) for point {700A180063}> #<sb-kernel:layout for point, INVALID=:uninitialized {7007551CE3}>)
1: (sb-kernel::%defstruct #<sb-kernel:defstruct-description point {70074B6DA3}> #(#<sb-kernel:layout for t {7003033803}> #<sb-kernel:layout (ID=1) for structure-object {7003033883}>) #S(sb-c:definition-source-location :namestring nil :indices 0))
...
The debugger has essentially paused code execution, and is waiting for you to select a restart. You can select the restart by pressing Ctrl + r and then pressing 0 or 1 (or another restart number). Suppose we select the restart numbered 0.
The redefinition of the point structure-class would proceed. However
*point*
#=> #<UNPRINTABLE instance of #<structure-classoid point> {700A37C8B3}>
point-p(*point*)
#=> nil
Unlike classes, where the redefinition of class resulted in a clean updation of instances, redefinition of structures requires a fair bit of manual work. To actually use the new definition of point structure-class, you will need to redefine the binding for *point*.
defparameter *point* = make-point(:x, 3, :y, 4)
*point*
#=> #S(point :x 3 :y 4 :z 5)
point-p(*point*)
#=> t
Performance
To actually compare the performance of classes and structures, let us define an equivalent class and a structure:
defclass point-class():
slots:
x:
initarg: :x,
accessor: class-x;
y:
initarg: :y,
accessor: class-y;
end
end
defstruct point-struct:
x;
y;
end
Then one can run a loop summing up the x and y a million times.
For the class:
let point = make-instance($point-class, :x, 3, :y, 4),
num-iter = 1e9,
sum = 0:
time loop :repeat num-iter :do
sum = sum + class-x(point) + class-y(point)
end
sum
end
Evaluation took:
13.849 seconds of real time
13.865638 seconds of total run time (13.843339 user, 0.022299 system)
100.12% CPU
0 bytes consed
For the structure:
let point = make-point-struct(:x, 3, :y, 4),
num-iter = 1e9,
sum = 0:
time loop :repeat num-iter :do
sum = sum + point-struct-x(point) + point-struct-y(point)
end
sum
end
Evaluation took:
12.229 seconds of real time
12.229539 seconds of total run time (12.215391 user, 0.014148 system)
100.01% CPU
0 bytes consed
At first, this looks comparable. However, one can specify the types of the structure slots:
defstruct point-struct:
(x = 0) :: fixnum;
(y = 0) :: fixnum;
end
Then
let point = make-point-struct(:x, 3, :y, 4),
num-iter = 1e9,
sum = 0:
declare type(fixnum, sum)
time loop :repeat num-iter :do
sum = sum + point-struct-x(point) + point-struct-y(point)
end
sum
end
We are down to a third of the time!
Evaluation took:
4.223 seconds of real time
4.223535 seconds of total run time (4.219177 user, 0.004358 system)
100.02% CPU
0 bytes consed
Meanwhile, type specification on classes have little impact:
defclass point-class():
slots:
x:
type: fixnum,
initarg: :x,
accessor: class-x;
y:
type: fixnum,
initarg: :y,
accessor: class-y;
end
end
let point = make-instance($point-class, :x, 3, :y, 4),
num-iter = 1e9,
sum = 0:
declare type(fixnum, sum)
time loop :repeat num-iter :do
sum = sum + class-x(point) + class-y(point)
end
sum
end
Evaluation took:
15.313 seconds of real time
15.305151 seconds of total run time (15.280037 user, 0.025114 system)
100.01% CPU
0 bytes consed
The gap becomes even more pronounced when one considers construction of new instances and more complex read or write operations.
In practice, there are projects such as static-dispatch and fast-generic-functions that attempt to overcome the performance limitations of generic functions and standard classes, trying to give you the best of both worlds. However, these are not standard. The standard way to obtain fast Moonli (or Common Lisp) code is to use structures.
Classes vs Structures
Classes are perfect for flexible, evolving designs. Structures are ideal for performance-critical code.
| Feature | Classes | Structures |
|---|---|---|
| Single inheritance | ✅ | ✅ |
| Multiple inheritance | ❌ | ❌ |
| Dynamic redefinition | ✅ | ❌ |
| Performance | ❌ | ✅ |
| Use with generic functions | ✅ | ✅ |
Feedback
Was this page helpful?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.