Skip to content

String building in Lisp

We’re grinding (in the RPG sense) with lisp at work, and keep coming into problems that have multiple simple solutions, but don’t have the instincts/knowledge to readily identify which one is best. One such problem is building up strings, in a couple of cases. I took some time this morning to go through the cases, using a 10000 iteration loop and time to do a simple benchmark. The tests were run on SBCL 1.0.11.

UPDATE: added metrics with *PRINT-PRETTY* off, as suggested in comments. This has a big effect.

Conversion

There are some cases where we wanted a number as a string, as part of building HTML. For this demo, we’ll put the number in variable *d*:

(defvar *d* 19.80)

We found three options:

  1. cl-interpol: #?”${*d*}” => “19.8”
  2. (princ-to-string *d*) => “19.8”
  3. (format nil “~a” *d*) => “19.8”

Here’s how they compared:

method bytes consed. page faults calls to %EVAL system run time (s) user run time (s) real time (s)
cl-interpol, *PRINT-PRETTY* nil 5,677,440 0 0 0.0 0.085987 0.087
cl-interpol 22,964,840 0 0 0.001 0.138979 0.137
princ-to-string, *PRINT-PRETTY* nil 0 0 0 0.0 0.0 0.0
princ-to-string 0 0 0 0.0 0.0 0.0
format, *PRINT-PRETTY* nil 6,483,008 0 0 0.0 0.110984 0.111
format 23,763,096 0 0 9.99e-4 0.166974 0.168

Princ-to-string is the bigger winner in this case.

Simple Prepending

For the this case, we have some decimal value in a variable called *d*, and want it in a string, prepended with a “$”.

We identified three possible solutions:

  1. (arnesi:join-strings (list “$” (princ-to-string *d*))) => “$19.8”
  2. cl-interpol: #?”$${*d*}” => “$19.8”
  3. (format nil “$~a” *d*) => “$19.8”

Here’s how they compared:

method bytes consed. page faults calls to %EVAL system run time (s) user run time (s) real time (s)
arnesi:join-strings, *PRINT-PRETTY* nil 6,320,064 0 0 9.99e-4 0.099985 0.1
arnesi:join-strings 23,598,928 0 0 0.002 0.158975 0.159
cl-interpol, *PRINT-PRETTY* nil 5,675,904 0 0 0.0 0.078988 0.079
cl-interpol 22,961,800 0 0 0.001 0.144978 0.144
format, *PRINT-PRETTY* nil 6,801,568 0 0 0.0 0.122982 0.122
format 24,085,088 0 0 0.0 0.186971 0.186

In this case, cl-interpol wins on all counts, after compile time.

Dynamic Prepending

In this case, our prefix is determined by another variable:

(defvar *prefix* "$")

  1. (arnesi:join-strings (list *prefix* (princ-to-string *d*))) => “$19.8”
  2. cl-interpol: #?”${*prefix*}${*d*}” => “$19.8”
  3. (format nil “~a~a” *prefix* *d*) => “$19.8”
method bytes consed. page faults calls to %EVAL system run time (s) user run time (s) real time (s)
arnesi:join-strings, *PRINT-PRETTY* nil 6,319,784 0 0 0.0 0.099985 0.101
arnesi:join-strings 23,602,432 0 0 0.001 0.155977 0.156
cl-interpol, *PRINT-PRETTY* nil 5,679,672 0 0 0.0 0.081987 0.083
cl-interpol 40,239,976 0 0 0.0 0.19597 0.196
format, *PRINT-PRETTY* nil 7,282,504 0 0 0.002 0.13298 0.131
format 41,845,632 0 0 0.001 0.234964 0.234

Now things get a little messier, and dependent on the setting of *PRINT-PRETTY*. join-strings wins big if *PRINT-PRETTY is on, but if its off, then cl-interpol takes the cake again.

Simple Wrapping

In this case we want to put some text before and after our value.

The options:

  1. (arnesi:join-strings (list “$” (princ-to-string *d*) ” USD”)) => “$19.8 USD”
  2. cl-interpol: #?”$${*d*} USD” => “$19.8 USD”
  3. (format nil “$~a USD” *d*) => “$19.8 USD”

The data:

method bytes consed. page faults calls to %EVAL system run time (s) user run time (s) real time (s)
arnesi:join-strings, *PRINT-PRETTY* nil 6,722,200 0 0 0.0 0.095985 0.096
arnesi:join-strings 24,001,352 0 0 0.001 0.165975 0.163
cl-interpol, *PRINT-PRETTY* nil 5,845,912 0 0 0.0 0.093986 0.094
cl-interpol 23,122,736 0 0 0.001 0.147977 0.149
format, *PRINT-PRETTY* nil 7,439,600 0 0 0.0 0.13298 0.132
format 24,725,176 0 0 0.001 0.19897 0.199

cl-interpol is still the winner, which is expected given the results of the simple prepending.

Dynamic Wrapping

Now let’s put the suffix into a variable and see how it goes:

(defvar *suffix* "USD")

The options:

  1. (arnesi:join-strings (list *prefix* (princ-to-string *d*) ” ” *suffix*)) => “$19.8 USD”
  2. cl-interpol: #?”${*prefix*}${*d*} ${*suffix*}” => “$19.8 USD”
  3. (format nil “~a~a ~a” *prefix* *d* *suffix*)=> “$19.8 USD”

The results:

method bytes consed. page faults calls to %EVAL system run time (s) user run time (s) real time (s)
arnesi:join-strings, *PRINT-PRETTY* nil 6,964,576 0 0 0.0 0.100985 0.101
arnesi:join-strings 24,248,160 0 0 0.001999 0.166975 0.168
cl-interpol, *PRINT-PRETTY* nil 5,842,032 0 0 0.0 0.099985 0.099
cl-interpol 57,682,928 0 0 0.001 0.250961 0.251
format, *PRINT-PRETTY* nil 8,560,800 0 0 0.0 0.155977 0.157
format 60,406,968 0 0 0.001 0.341948 0.342

Another mixed result, depending on *PRINT-PRETTY*. Again we have join-strings if you need *PRINT-PRETTY*, cl-interpol if not.

My Conclusions

Use princ-to-string for simple conversions, use cl-interpol everywhere if you can turn off *PRINT-PRETTY*. If I need *PRINT-PRETTY*, then use cl-interpol and replace it with join-strings when tuning performance.

4 Comments