编写快速的 Common Lisp 代码

Write fast Common Lisp code

我不确定,如果一些奇怪的事情让我的代码更快:

通常使用内置操作或编写新的专用函数来做同样的事情更好吗? (例如 #'map 的版本仅适用于向量;我的版本通常没有类型声明更快)

我应该定义新的(复杂的)类型以在声明中使用它们吗? (例如类型列表)

我应该直接为对象定义插槽吗? (例如 pxpy 用于二维对象,或者是否可以使用向量类型的一个插槽 pos,以便我可以将其重新用于其他目的)

这有几个部分,但这里是一个快速的头脑风暴

个人资料!

使用内置分析器的 CL 发行版,例如我使用 sbcl http://www.sbcl.org/1.0/manual/Statistical-Profiler.html

sbcl 探查器的好处在于,一旦您探查了一个函数,如果您对其进行反汇编,机器代码就会用统计信息进行注释。这需要对目标机器代码有一定的了解。

不要低估您的实施:他们可以内置高级类型和流分析,并且能够,例如,在有意义的时候选择地图的纯矢量版本。

了解编译器宏:编译器宏可以隐藏函数,这为您提供了一个根据表单上下文进行额外优化的地方。 IT 在不替换功能的情况下执行此操作,因此它仍然可以以更高阶的方式使用。

学习类型声明

我发现这一系列博文帮助我理解了这项技术http://nklein.com/tags/optimization/page/2/全部阅读!

重要提示:永远不要在类型问题上对编译器撒谎。类型声明是一种告诉编译器你知道类型是什么的方式,编译器甚至不必使用它们,当它使用时也不必检查你给它的是正确的东西。

未装箱数据

一些实现能够在特定条件下对特定数据类型进行拆箱。抱歉,这含糊不清,但您需要仔细阅读以了解您的实施情况。对于 sbcl,'sbcl internals' 指南非常有用。

例如:

(make-array 100 :element-type 'single-float :initial-element 0.0)

可以作为连续的内存块存储在 sbcl 中。

再次剖析(使用真实数据)

我花了 3 个小时编写了一个疯狂的基于 n 维矩阵乘法例程的编译器宏,然后针对 1 行内置解决方案对其进行了测试。对于 5 维以下的矩阵,差别不大!对于更高的维度,是的,它震撼了,但是 'performance benefit' 纯粹是学术性的,因为那些代码路径从未被触及过。幸运的是,我是为了好玩而承担这项任务的,因为我现在问的是同样的问题。

算法

世界上所有的类型说明符都不会给你100倍的性能提升。这来自更好的技术。阅读问题背后的数学原理,实现具有不同优势的不同辅助函数,并在运行时在它们之间进行选择……然后返回并使用编译器宏允许 lisp 在编译时进行选择。或将技术指定为高阶函数,例如 make-hash-table 允许您指定散列函数和重新散列大小,这对于在某些大小下获得良好性能至关重要。

了解 BigO 的极限

如果由于内存局部性问题而失去所有性能,算法复杂性就毫无意义。相反,有时我们可以实现超线性性能特征,如果通过在核心之间拆分问题,减少的数据集现在适合 l2 缓存。

BigO 是一个很好的指标,但它还没有结束。这就是为什么关联列表对于少量密钥和某些访问配置文件是哈希表的完全有效替代方案。

总结

我从 lisp 社区的某个地方听到了一个非常有效的金咒:

Make it Fast and then make it Fast

如果不出意外,请遵循此。给自己唱吧!

快速启动程序并运行,这样做您更有可能发现可以使用更好的技术或算法来获得几个数量级改进的地方。 先用CL自己的功能。不要过早地使用宏来交换 lisp 的高阶性质,探索你可以用函数走多远。

[编辑] 更多注释 - 以下是针对 sbcl

  • 结构槽上的类型定义用于优化,class 槽的类型声明不是。
  • 关于类型,从使程序易于编写和理解(让它快)开始,然后查看访问时间是否是瓶颈(让它快!)
  • (slot-value x 'name) 当名称已知时非常快。看看 with-slots 如何利用 symbol-macrolet 发挥其优势

所以直接回答你原来的问题:

  • 首先内置(同时检查库)
  • 它是否使问题更容易编写和理解?
  • 使用位置。到该间接的性能成为问题时,您将找到许多其他方法来加速问题,并且解决方案将成为更广泛的优化技术的一部分。