A fast MAPCONCAT implementation in Common Lisp

Here’s an implementation of Emacs Lisp’s MAPCONCAT function for Common Lisp.

(defun mapconcat (fun list sep)
   (when list
      (let ((~sep (with-output-to-string (*standard-output*)
                     (map nil (lambda (ch) (princ (if (char= #~ ch) "~~" ch))) sep))))
       (format nil (format nil "~~A~~{~A~~A~~}" ~sep)
               (funcall fun (first list))
               (mapcar fun (rest list))))))

timed :

* (time (mapconcat 'identity *mylist* "-"))

 Evaluation took:
   2.805 seconds of real time
   2.746358 seconds of total run time (2.736834 user, 0.009524 system)
   [ Run times consist of 0.004 seconds GC time, and 2.743 seconds 
     non-GC time. ]
   97.90% CPU
   6,324,642,149 processor cycles
   17,734,520 bytes consed

 "0-1-2-3-4-5-6-7-8-9-10- ... "

And here’s an optimized version.

(setf *mylist*
      (let ((l (list 0)))
        (dotimes (i 10000 i) (nconc l (list (write-to-string i))))
        (cdr l)))

(defun mapconcat(func lst sep)
  (let ((vs (make-array 0
                        :element-type 'character
                        :fill-pointer 0
                        :adjustable t)))
    (dotimes (i (length lst) i)
      (let ((str (funcall func (nth i lst))))
        (dotimes (j (length str) j)
          (vector-push-extend (char str j) vs))
        (dotimes (k (length sep) k)
          (vector-push-extend (char sep k) vs))))
    vs)) 

timed :

* (time (mapconcat 'identity *mylist* "-"))

 Evaluation took:
   0.133 seconds of real time
   0.098758 seconds of total run time (0.098390 user, 0.000368 system)
   74.44% CPU
   299,046,898 processor cycles
   517,800 bytes consed

 "0-1-2-3-4-5-6-7-8-9-10- ... "

As someone pointed out the following would be much faster using FORMAT’s powerful directives and turning off *pretty-print* :

(defun mapconcat (function list elem)
  (let (*print-pretty*)
    (format nil (format nil "~~{~~a~~^~a~~}" elem)
            (mapcar function list)))) 

timed :

* (time (mapconcat 'identity *mylist* "-"))

Evaluation took:
  0.006 seconds of real time
  0.005033 seconds of total run time (0.005001 user, 0.000032 system)
  83.33% CPU
  11,430,579 processor cycles
  539,200 bytes consed

 "0-1-2-3-4-5-6-7-8-9-10- ... "

However, the FORMAT version does not demonstrate a fast CL implementation.
To get the low-level implementation to match the performance of the FORMAT implementation we simply make a few tweaks.
(We replace DOTIMES/NTH for the outer loop with MAPCAR as NTH is slow).

(defun mapconcat(func lst sep)
  (declare (type (cons (simple-array character (*))) lst))
  (declare (type (simple-array character (*)) sep))
  (let ((vs (make-array 0
                        :element-type 'character
                        :fill-pointer 0
                        :adjustable t))
        (lsep (length sep)))
    (mapcar #'(lambda (str)
                (let ((nstr (funcall func str)))
                  (declare (type (simple-array character (*)) nstr))
                  (dotimes (j (length nstr) j)
                    (declare (type fixnum j))
                    (vector-push-extend (char nstr j) vs))
                  (dotimes (k lsep k)
                    (declare (type fixnum k))
                    (vector-push-extend (char sep k) vs))))
                lst)
    vs))

timed :

* (time (mapconcat 'identity *mylist* "-"))

Evaluation took:
  0.006 seconds of real time
  0.005435 seconds of total run time (0.005261 user, 0.000174 system)
  83.33% CPU
  11,845,515 processor cycles
  605,792 bytes consed

 "0-1-2-3-4-5-6-7-8-9-10- ... "

(all versions timed on SBCL 1.0.29, OS X 10.5.7, 2.26 GHz core 2 duo macbook)

Leave a Reply

Your email address will not be published. Required fields are marked *