First question: Is it reasonable to assume that a modern compiler for Common Lisp will usually compile (mapcar #'fn ...)
and (map 'list #'fn ...)
into the same code as (mapc #'fn ...)
? That is, is it reasonable to assume that a compiler will see that the return value is ignored, so that a new list doesn't need to be constructed? e.g. suppose that my source file contains this code:
(defun set-foo-5 (sym)
(setf (get sym 'foo) 5))
(progn
(mapcar #'set-foo-5 '(a b c))
(format t "All foos are five!~%"))
Would mapc
be more efficient? I usually run SBCL, but my guess is that any good compiler would be able to figure out that there's no need to cons up a new list in this situation. Am I right?
Second question: In the same situation, should I assume that a modern compiler will usually compile map 'list
into the same code as mapcar
, as long as 'list
is there in the source code, and not chosen at run time?
Third question: Similar question for other sequences. For example, if I replace the mapcar
line in the progn above with (map 'vector #'set-foo-5 #(a b c))
, should I assume that that the compiled code will not bother to construct a new vector?
First of all, map
and mapcar
differ in a very important way: the latter operates on lists while the former works with any sequences. This means that (map nil ...)
(or (map 'list ...)
) is equivalent to (mapc ...)
(resp. (mapcar ...)
) only if the compiler can prove that all the data arguments are list
s.
Second, most modern Lisp compilers (e.g., SBCL) are usually good enough to figure out such things (with declarations, when necessary).
Third, the only way to be sure is to use disassemble
.
Fourth, the function choice is a way to document your code. When you use map
, you are telling the human reader of your code that you will be passing non-lists to the function. If you are sure that only lists will be used, why would you want to confuse the reader (yourself a few months later)?
Fifth, premature optimization is the root of all evil.
Using some of the tips in @sds's answer, I realized that there are simple preliminary tests that might answer my question for a specific implementation (without having to work through dissemble
output). It appears that SBCL and CCL do not necessarily treat mapcar
the same way as mapc
when mapcar
's return value is ignored. First define lis1
and lis2
as lists of significant length (mine were length 100,000, and contained integers), then run mapcar
, mapc
, etc. over them many times, as in the the following (with optional preliminary calls to gc
to clear out old garbage):
(gc :full t)
(time
(progn
(dotimes (ignored 1000)
(mapcar #'+ lis1 lis2))
(format t "mapcar:~%")))
(gc :full t)
(time
(progn
(dotimes (ignored 1000)
(mapc #'+ lis1 lis2))
(format t "mapc:~%")))
(gc :full t)
(time
(progn
(dotimes (ignored 1000)
(map nil #'+ (the list lis1) (the list lis2)))
(format t "map nil with lists~%")))
For example, SBCL on my machine produces:
mapcar:
Evaluation took:
2.306 seconds of real time
2.287627 seconds of total run time (2.136130 user, 0.151497 system)
[ Run times consist of 0.147 seconds GC time, and 2.141 seconds non-GC time. ]
99.22% CPU
3,683,188,504 processor cycles
1,600,049,536 bytes consed
mapc:
Evaluation took:
0.639 seconds of real time
0.638733 seconds of total run time (0.638011 user, 0.000722 system)
100.00% CPU
1,020,310,296 processor cycles
0 bytes consed
map nil with lists
Evaluation took:
0.592 seconds of real time
0.592114 seconds of total run time (0.591199 user, 0.000915 system)
100.00% CPU
945,957,944 processor cycles
0 bytes consed
These are typical results with default optimization settings. Using declaim
to optimize for speed, non-safety, etc. speeds things up a little bit but doesn't change the fact that mapc
and map nil
are an order of magnitude faster than mapcar
, and that mapcar
does a lot of consing. Results for CCL are similar, but slower overall.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With