Customizing Common Lisp's Iterate: Timing

Posted on October 10th, 2016.

This entry is the second in a series about customizing Common Lisp's iterate library. The full series:

This post will show how to make a timing clause that keeps track of how much time has elapsed between iterations (and across the entire loop). I've found it handy when doing rough benchmarks for things like maze generation.

  1. End Result
  2. Code
    1. Argument Pairing
    2. Symbols & Keywords
    3. Variables
    4. Splicing
  3. Composing

End Result

Before we look at the code, let's look at what we're aiming for:

(defun to-seconds (internal-time-units)
  (/ internal-time-units internal-time-units-per-second 1.0))

(iterate
  (repeat 5)
  (sleep (random 3.0))
  (timing real-time :since-start-into total-time
                    :per-iteration-into loop-time)
  (collect loop-time :into loop-times)
  (finally
    (format t "Individual times: ~S~%Total time: ~F seconds~%"
            (mapcar #'to-seconds loop-times)
            (to-seconds total-time))))
; =>
Individual times: (0.948 1.359 2.828 2.891 2.744)
Total time: 10.77 seconds

The timing clause is what we'll be implementing.

Code

The code for timing is fairly short but does have a few interesting things to talk about. I've omitted the docstring so it'll fit on one screen:

(defmacro-clause (TIMING time-type &optional
                  SINCE-START-INTO since-var
                  PER-ITERATION-INTO per-var)
  (let ((timing-function (ecase time-type
                           ((real-time) #'get-internal-real-time)
                           ((run-time) #'get-internal-run-time)))
        (since-var (or since-var
                       (when (null per-var)
                         iterate::*result-var*))))
    (with-gensyms (start-time current-time previous-time)
      `(progn
        (with ,start-time = (funcall ,timing-function))
        (for ,current-time = (funcall ,timing-function))
        ,@(when since-var
            `((for ,since-var = (- ,current-time ,start-time))))
        ,@(when per-var
            `((for ,previous-time :previous ,current-time
                                  :initially ,start-time)
              (for ,per-var = (- ,current-time ,previous-time))))))))

Times are recorded in "internal time units" to be consistent with other Common Lisp functions like get-internal-real-time. Some implementations will be more precise than others.

Argument Pairing

One thing that I sometimes find annoying about iterate clauses is that they must contain pairs of arguments. You can't have an odd number of arguments, like:

(TIMING &optional SINCE-START since-var PER-ITERATION per-var).

Often, however, you can make the best of this restriction and come up with something at least moderately useful to use as an extra argument to pad things out. For the timing clause I decided to make the second argument define which type of time to measure (process run time or wall clock time).

Symbols & Keywords

In this implementation the type of time to record is given as a symbol in this package's namespace. This means that you'll want to export the real-time and run-time symbols from this package, not just the timing symbol. This is a little annoying, but I like how it looks when used.

Another option would be to change these to keyword symbols, so you would say (timing :real-time ...) instead. Or you could just support both (ecase makes this easy).

Note the use of ecase when checking the time type to signal an error at macroexpansion time with a helpful message if the user passed something incorrect:

(iterate (timing foo :since-start-into some-var))
; =>
debugger invoked on a SB-KERNEL:CASE-FAILURE in thread
 #<THREAD "main thread" RUNNING {10028F7473}>:
   FOO fell through ECASE expression. Wanted one of (REAL-TIME RUN-TIME).

...

Variables

To fit with the general pattern of iterate clauses, we try to do something intelligent with the variables depending on what the user says they want. If the user doesn't specify any variable names, we return the total loop time as the result of the loop:

(macroexpand-iterate '(timing real-time))
; =>
(PROGN
 (WITH #:START-TIME516 = (FUNCALL #<CLOSURE GET-INTERNAL-REAL-TIME>))
 (FOR #:CURRENT-TIME517 = (FUNCALL #<CLOSURE GET-INTERNAL-REAL-TIME>))
 (FOR ITERATE::*RESULT-VAR* = (- #:CURRENT-TIME517 #:START-TIME516)))

(iterate (repeat 1) (sleep 2.0) (timing real-time))
; =>
2003

There's a bit of inefficiency here, because we're recalculating the time on every iteration even though we really only need to do it once at the very end. But I suppose someone could be accessing iterate::*result-var* in their loop body like any other variable, so this will do the right thing in that case (even if that case is horrifying).

If the user does specify a variable for either measurement, we assume they know what they're doing and don't touch the loop result variable:

(macroexpand-iterate
  '(timing real-time :since-start-into x))
; =>
(PROGN
 (WITH #:START-TIME525 = (FUNCALL #<CLOSURE GET-INTERNAL-REAL-TIME>))
 (FOR #:CURRENT-TIME526 = (FUNCALL #<CLOSURE GET-INTERNAL-REAL-TIME>))
 (FOR X = (- #:CURRENT-TIME526 #:START-TIME525)))

(macroexpand-iterate
  '(timing real-time :per-iteration-into x))
; =>
(PROGN
 (WITH #:START-TIME528 = (FUNCALL #<CLOSURE GET-INTERNAL-REAL-TIME>))
 (FOR #:CURRENT-TIME529 = (FUNCALL #<CLOSURE GET-INTERNAL-REAL-TIME>))
 (FOR #:PREVIOUS-TIME530 :PREVIOUS #:CURRENT-TIME529 :INITIALLY
  #:START-TIME528)
 (FOR X = (- #:CURRENT-TIME529 #:PREVIOUS-TIME530)))

Splicing

Another thing to note is the use of ,@ to splice the appropriate clauses into the final progn:

; ...
        ,@(when since-var
            `((for ,since-var = (- ,current-time ,start-time))))
        ,@(when per-var
            `((for ,previous-time :previous ,current-time
                                  :initially ,start-time)
              (for ,per-var = (- ,current-time ,previous-time))))
; ...

For the since-var clause we could have gotten away with just using a plain , and returning the for clause itself:

; ...
        ,(when since-var
           `(for ,since-var = (- ,current-time ,start-time)))
; ...

This would work, but when since-var is null (when the user wants only the per-iteration time) the when statement would evaluate to nil, and would leave a stray nil in the progn:

(macroexpand-iterate
  '(timing real-time :per-iteration-into x))
; =>
(PROGN
 (WITH #:START-TIME531 = (FUNCALL #<CLOSURE GET-INTERNAL-REAL-TIME>))
 (FOR #:CURRENT-TIME532 = (FUNCALL #<CLOSURE GET-INTERNAL-REAL-TIME>))
 NIL
 (FOR #:PREVIOUS-TIME533 :PREVIOUS #:CURRENT-TIME532 :INITIALLY
  #:START-TIME531)
 (FOR X = (- #:CURRENT-TIME532 #:PREVIOUS-TIME533)))

Any decent compiler will optimize this away, but it's ugly to read during macroexpansion. If we're going to write this thing let's do it right and not be lazy.

Also notice that we don't define the previous-time clause at all unless the user wants per-iteration time, because the since-start time doesn't need it at all. No reason to waste cycles calculating an extra value on every iteration if we don't have to.

Composing

One of the great things about iterate is that the extensions you make can use each other. For example, we can use last post's averaging clause with the one we just made to calculate the average time per loop iteration:

(iterate
  (repeat 20)
  (sleep (random 1.0))
  (timing real-time :per-iteration-into time)
  (averaging (to-seconds time)))
; =>
0.51555