I am working on some Lisp code. I am trying to mimic the basic structure of a large C++ project. I think the way the C++ project is structured is a good fit for the tasks involved.
Most of the C++ stuff is done with classes. Most of the methods of those classes are virtual methods. Many of the methods will be called multiple times every hundredth of a second. Very few of the virtual methods ever get overridden by subclasses.
So, I got to thinking. Does it make sense for me to use CLOS stuff at all for most of this? Would it be significantly faster to use (defstruct …) and (defun …) instead of (defclass …) and (defgeneric …)?
My gut instinct was: Yes.
My first set of test code didn’t bear that out. For both the classes and the structs, I used (MAKE-INSTANCE …) to allocate and (WITH-SLOTS …) to access. When doing so, the classes with generic functions took about 1.5 seconds for 10,000,000 iterations under SBCL while the structs with non-generic functions took about 2.2 seconds.
For the next iteration of testing, I decided to use accessors instead of slots. I had the following defined:
((slot-one :initform (random 1.0f0) :type single-float
:accessor class-slot-one)
(slot-two :initform (random 1.0f0) :type single-float
:accessor class-slot-two)))
(defclass sub-class (base-class)
((slot-three :initform (random 1.0f0) :type single-float
:accessor class-slot-three)
(slot-four :initform (random 1.0f0) :type single-float
:accessor class-slot-four)))
(defstruct (base-struct)
(slot-one (random 1.0f0) :type single-float)
(slot-two (random 1.0f0) :type single-float))
(defstruct (sub-struct (:include base-struct))
(slot-three (random 1.0f0) :type single-float)
(slot-four (random 1.0f0) :type single-float))
I then switched from using calls like (slot-value instance ‘slot-three) in my functions to using calls like (class-slot-three instance) or (sub-struct-slot-three instance). Now, 10,000,000 iterations took 2.6 seconds for the classes and 0.3 seconds for the structs in SBCL. In Clozure (64-bit), 10,000,000 iterations with classes and with method-dispatch and with accessors took 11.0 seconds, with classess and without method-dispatch but with accessors took 6.1 seconds, and with structs and without method-dispatch and with accessors took 0.4 seconds.
There is much more that I can explore to tune this code. But, for now, I think the answer is fairly easy. I am going to use structs, functions, and accessors for as many cases as I can. Here is the generic-function, class, struct test code with accessors. The first loop uses classes and generic functions. The second loop uses classes and regular functions. The third loop uses structs and regular functions.
So they’re both so fast it doesn’t matter which you use, but you’re going to go with structs?
The structs are five times faster. Certainly, both are fast enough for almost everything. But, for realtime, interactive 3D stuff, there is enough going on that some of those 5x’s could make a big difference. Certainly, I don’t want it to only be able to do 1/2 the resolution or a quarter of the polygons that the C++ version does if I can help it.
Yes, this falls in the “premature optimization” category. But, it also falls in the “easier to do now” and “just about as pretty” and “some of it will have thousands of accesses per frame” categories.
Isn’t there a way you can write your Lisp code so that even if you start out using structures you can make it very easy to migrate to objects later on?
There are probably ways. I would have to experiment quite a bit to get a way that I felt confident would work throughout. I have since been reconsidering my stance on this. I think I’m still going to use classes and generic functions for things like cameras and meshes that I expect to get called only a couple of times per frame and use structs for things like facets and vertexes that will be referenced hundreds of thousands of times per frame.
I still haven’t fully embraced Agile programming, but I have done enough fundamental changes to APIs in my time, that I’m reasonably confident I can go back and retro-fit whatever I need to if some portion is weighting me down.