我在“On Lisp”一书中读到,应该避免在扩展宏的主体中过度使用cons
。为什么cons
被认为是效率低下的操作? Lisp不与cons细胞共享结构吗?
答案 0 :(得分:40)
你需要先理解行话。
CONS 是创建新的cons单元格的原始操作。
Consing 意味着通常在堆上分配内存,不仅仅是缺陷单元:数字的构成,数组的构造,CLOS对象的构造,......
“内存管理术语表”中的定义说:
所以,格雷厄姆使用了第二个含义。
如果格雷厄姆说'避免特别费钱。一种不必要地使用的实用程序可能破坏其他有效程序的性能。 - 这意味着:避免堆上不必要的内存分配。但对于任何代码 - 宏,函数等都是如此。
当计算机内存较少且垃圾收集成本较高时(尤其是需要在虚拟内存系统中扫描换出的内存时,物理内存小于虚拟内存),尤其如此。
今天的问题不是一个问题,但仍然具有相关性。
以函数READ-LINE为例,它从流中读取一行并“汇集”一个新字符串。请注意,字符串不是由conses构建的,而是一个向量。我们的意思是'在堆上分配一个字符串'。如果你有一个包含大量行的大文件,那么使用一个获取缓冲区的例程并使用行字符填充缓冲区可能会更快(Common Lisp中有一些带有填充指针的向量)。这样,行缓冲区只是一个对象,可以重用于对此行读取函数的调用。
请参阅Allegro CL文档中的此示例:Some techniques for reducing consing when processing very large text files。
答案 1 :(得分:7)
我认为cons不是很慢。它不会创建整个列表的新副本,只是将新元素附加到链接列表的前面。
如果任何事情都很慢,那就是它必须为新节点分配一些内存。如果您要创建大量节点,最好一次创建它们而不是一个一个。
答案 2 :(得分:6)
实际上,在良好的实现中,consing的速度相当快,但是可以通过避免它来获得更好的性能。如果您更改的内容是由您自己创建的,则可以安全地使用破坏性操作,如下例所示:
CL-USER> (defun non-destructive ()
(progn
(reverse (loop for x from 1 to 100000 collecting x))
nil))
NON-DESTRUCTIVE
CL-USER> (defun destructive ()
(progn
(nreverse (loop for x from 1 to 100000 collecting x))
nil))
DESTRUCTIVE
CL-USER> (time (non-destructive))
(NON-DESTRUCTIVE) took 140 milliseconds (0.140 seconds) to run
with 2 available CPU cores.
During that period, 156 milliseconds (0.156 seconds) were spent in user mode
0 milliseconds (0.000 seconds) were spent in system mode
94 milliseconds (0.094 seconds) was spent in GC.
1,600,024 bytes of memory allocated.
NIL
CL-USER> (time (destructive))
(DESTRUCTIVE) took 93 milliseconds (0.093 seconds) to run
with 2 available CPU cores.
During that period, 93 milliseconds (0.093 seconds) were spent in user mode
0 milliseconds (0.000 seconds) were spent in system mode
63 milliseconds (0.063 seconds) was spent in GC.
800,024 bytes of memory allocated.
NIL
所以:是的,避免收费可以提高性能,但是如果你知道自己在做什么,那么你应该只使用破坏性修改。我不会说消费本身是“缓慢的”,但尽管如此,避免它可能是有益的。如果你比较分配内存的差异(花费时间),应该清楚为什么第二个版本比第一个版本更快。
答案 3 :(得分:2)
Consing,这是用于动态内存分配的Lisp术语,并不慢。但它增加了程序的开销。您不仅可以查看分配对象的成本,还可以考虑生命周期的全部成本,从创建对象到在变为垃圾时回收它。
对垃圾收集者施加不必要的“施加压力”;它使垃圾收集工作更频繁。
(这不是Lisp问题。例如,对malloc
和free
进行大量调用的C程序,或使用new
和delete
的C ++程序lot也不会像类似的程序一样好,这些程序组织得更好以避免许多这些调用。)
你不必为了避免在Lisp中使用而产生偏执,因为这会使编程变得不方便,并且一些避免耗费的技巧,包括对现有对象的破坏性重用,都容易出错。
但是,在内部循环中执行很多次的情况下,以及在用于广泛重用的实用程序函数等低级代码中,需要注意的事情。例如,假设mapcar
内部反向构建结果列表,然后通过调用reverse
而不是nreverse
将其按正确的顺序排列。然后,调用mapcar
的所有内容都会遭受不必要的惩罚。