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 »

  1. Zach Beane said,

    December 26, 2007 @ 4:38 pm

    FORMAT’s performance characteristics change dramatically if *PRINT-PRETTY* is nil. Check it out.

  2. ryan said,

    December 26, 2007 @ 5:22 pm

    Zach: updated to include *PRINT-PRETTY* settings. That is quite a difference.

  3. foobar said,

    December 26, 2007 @ 8:25 pm

    (concatenate ’string “foo” (princ-to-string 19.3) “bar”)

    (with-output-to-string (*standard-output*)
    (princ “foo”)
    (princ-to-string 19.3)
    (princ “bar”))

    Note that WITH-OUTPUT-TO-STRING also takes optionally a preallocated (even adjusting with fill-pointer) string - so you can reduce consing by reusing a string buffer.

  4. foobar said,

    December 26, 2007 @ 8:47 pm

    should be:

    (with-output-to-string (*standard-output*)
    (princ “foo”)
    (princ 19.3)
    (princ “bar”))

    or even:

    (with-output-to-string (*standard-output*) (mapc #’princ ‘(”foo” 19.3 “bar”)))

RSS feed for comments on this post · TrackBack URI

Leave a Comment