第一个问题:假设Common Lisp的现代编译器通常会将(mapcar #'fn ...)
和(map 'list #'fn ...)
编译成与(mapc #'fn ...)
相同的代码,这是否合理?也就是说,假设编译器会看到返回值被忽略,以便不需要构造新列表,这是否合理? 例如,假设我的源文件包含以下代码:
(defun set-foo-5 (sym)
(setf (get sym 'foo) 5))
(progn
(mapcar #'set-foo-5 '(a b c))
(format t "All foos are five!~%"))
mapc
会更有效率吗? 我通常运行SBCL,但我的猜测是,任何好的编译器都能够弄清楚在这种情况下没有必要编写新列表。 我说的对吗?
第二个问题:在同样的情况下,我是否应该假设现代编译器通常会将map 'list
编译成与mapcar
相同的代码,只要源代码中存在'list
,而不是在运行时选择?
第三个问题:其他序列的类似问题。 例如,如果我将上面 progn 中的 mapcar
行替换为 (map 'vector #'set-foo-5 #(a b c))
,我是否应该假设编译后的代码不会费心构造新的向量?
首先,map
和mapcar
有一个非常重要的区别:后者在列表上运行,而前者适用于任何序列。 这意味着(map nil ...)
(或(map 'list ...)
(等同于(mapc ...)
(或 (mapcar ...)
( 仅当编译器能够证明所有数据参数都是 list
s。
其次,大多数现代Lisp编译器(例如SBCL(通常足以弄清楚这些事情(必要时使用声明(。
第三,确定的唯一方法是使用disassemble
。
第四,函数选择是记录代码的一种方式。当你使用map
时,你告诉代码的人类读者,你将向函数传递非列表。如果您确定只会使用列表,为什么还要混淆读者(几个月后您自己(?
第五,过早优化是万恶之源。
使用 @sds 答案中的一些提示,我意识到有简单的初步测试可以回答我对特定实现的问题(无需完成dissemble
输出(。 看来SBCL和CCL不一定像mapc
一样对待mapcar
,当mapcar
的返回值被忽略时。 首先将 lis1
和 lis2
定义为长度很长的列表(我的长度为 100,000,包含整数(,然后在它们上运行 mapcar
、mapc
等多次,如下所示(可选择初步调用gc
清除旧垃圾(:
(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~%")))
例如,我的机器上的 SBCL 产生:
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
这些是默认优化设置的典型结果。 使用declaim
来优化速度、非安全性等可以加快速度,但并没有改变这样一个事实,即mapc
和map nil
比mapcar
快一个数量级,而且mapcar
做了很多事情。 CCL 的结果相似,但总体速度较慢。