Reading — step 1 of 5
Learn
Common Lisp is dynamically typed by default — but you can declare types and optimization levels and the compiler will produce code competitive with C.
Optimize declarations
(defun fast-sum (n)
(declare (optimize (speed 3) (safety 0) (debug 0))
(type fixnum n))
(let ((sum 0))
(declare (type fixnum sum))
(dotimes (i n sum)
(incf sum i))))
Qualities (0-3):
speed— runtime performancesafety— runtime checks (default 1)space— code/data sizedebug— debuggability (default 1)compilation-speed— compile time
(speed 3) (safety 0) — full speed, no checks. Use only after profiling.
Type declarations
(declare (type fixnum n)) ; small int (single machine word)
(declare (type integer n)) ; arbitrary precision
(declare (type single-float x)) ; 32-bit float
(declare (type double-float x)) ; 64-bit float
(declare (type (simple-array fixnum (*)) arr)) ; specialized array
(declare (type (vector double-float) v))
(declare (type string s))
The compiler uses these to:
- Skip dispatch / boxing
- Inline arithmetic
- Use unboxed array storage
Fixnum vs integer: fixnums fit in a machine word (typically up to ~2^61). Integer is arbitrary precision but slower. For tight loops with bounded values, fixnum is dramatically faster.
disassemble
See what the compiler generated:
(disassemble 'fast-sum)
For SBCL output is x86-64 assembly. Useful for understanding what (speed 3) actually buys you.
Inlining
(declaim (inline distance-squared))
(defun distance-squared (x1 y1 x2 y2)
(declare (type single-float x1 y1 x2 y2)
(optimize (speed 3) (safety 0)))
(let ((dx (- x2 x1))
(dy (- y2 y1)))
(+ (* dx dx) (* dy dy))))
The declaim inline tells the compiler to inline calls. Crucial for hot small functions.
Reading the compiler
SBCL emits notes — pay attention:
; note: doing float to pointer coercion (cost 13)
; to "<return value of FOO>"
This means SBCL had to box a float — slow. Adding a return-type declaration usually fixes it.
Specialized arrays
Generic arrays box every element. Specialized arrays don't:
(make-array 1000 :element-type '(unsigned-byte 8)) ; byte array, no boxing
(make-array 1000 :element-type 'fixnum) ; packed fixnums
(make-array 1000 :element-type 'double-float) ; packed doubles
Tail calls
Unlike Scheme, Common Lisp doesn't guarantee tail-call optimization — but most modern implementations (SBCL, CCL) do it for self-tail-calls and mutually-recursive ones. For portable code, prefer loop or do over deeply-recursive functions.
Profile before optimizing
The golden rule. SBCL has sb-profile:
(sb-profile:profile do-thing helper)
(my-app:run)
(sb-profile:report)
Reports per-function call counts and time. Optimize the slowest one first; ignore the rest.
Practical advice
- 80/20: declare types in the 5 hot functions; leave the rest dynamic
optimize (speed 3) (safety 1)is usually a sweet spot- Use
timemacro:(time (expensive-thing))reports CPU + memory - Premature optimization is still the root of evil — write correct code first, profile, then speed up the bottlenecks
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…