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:
- cl-interpol: #?”${*d*}” => “19.8”
- (princ-to-string *d*) => “19.8”
- (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:
- (arnesi:join-strings (list “$” (princ-to-string *d*))) => “$19.8”
- cl-interpol: #?”$${*d*}” => “$19.8”
- (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* "$")
- (arnesi:join-strings (list *prefix* (princ-to-string *d*))) => “$19.8”
- cl-interpol: #?”${*prefix*}${*d*}” => “$19.8”
- (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:
- (arnesi:join-strings (list “$” (princ-to-string *d*) ” USD”)) => “$19.8 USD”
- cl-interpol: #?”$${*d*} USD” => “$19.8 USD”
- (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:
- (arnesi:join-strings (list *prefix* (princ-to-string *d*) ” ” *suffix*)) => “$19.8 USD”
- cl-interpol: #?”${*prefix*}${*d*} ${*suffix*}” => “$19.8 USD”
- (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