common-lisp - Common Lisp SBCL 中函数调用的性能
问题描述
我是 Common Lisp 的新手,遇到了一个让我觉得很奇怪的性能问题。我正在循环使用 rem 检查一个数字是否可以被 10 整除。如果我将检查移到一个函数中,它的运行速度会慢 5 倍。什么会导致这种情况?
我在 64 位 Ubuntu 18.04 上运行 sbcl 1.4.5。
(defun fn (x)
(= 0 (rem x 10))
)
(defun walk-loop-local (n)
(loop for i from 1 to n do
(= 0 (rem i 10))
))
(defun walk-loop-func (n)
(loop for i from 1 to n do
(fn i)
))
(time (walk-loop-local 232792560))
(time (walk-loop-func 232792560))
我希望时间是一样的(而且要快得多,但这是一个单独的问题)。相反,这是输出,
CL-USER> (load "loops.lisp")
Evaluation took:
0.931 seconds of real time
0.931389 seconds of total run time (0.931389 user, 0.000000 system)
100.00% CPU
2,414,050,454 processor cycles
0 bytes consed
Evaluation took:
4.949 seconds of real time
4.948967 seconds of total run time (4.948967 user, 0.000000 system)
100.00% CPU
12,826,853,706 processor cycles
0 bytes consed
解决方案
Common Lisp 允许动态重新定义函数:如果您fn
在大约 第二次测试的 5 秒,运行循环将切换到调用新定义的fn
while running。此功能对如何编译函数调用以及如何在需要时优化它们有一些限制。
正如 RainerJoswing 在评论中指出的那样,以上是过度简化,在某些情况下编译器可能会假设函数没有重新定义(递归函数,同一文件中的函数),请参见3.2.2.3 语义约束,例如:
文件中对同一文件中定义的命名函数的调用引用该函数,除非该函数已声明为 notinline。如果函数在运行时单独重新定义或在同一文件中多次定义,后果是不确定的。
一个函数混合了错误检查和您希望它执行的计算。在函数调用边界处,您通常有一个序言来检查您的输入,以及一个结果可能被“装箱”的结语:如果编译器知道本地变量始终是单浮点数,它可以在期间使用浮点数的原始表示函数的范围,但是当返回结果时,它应该是一个有效的 Lisp 类型,例如,这意味着将其强制回一个标记值。
SBCL 编译器试图确保代码是安全的,其中安全意味着永远不会调用在 Lisp 规范中具有未定义行为的代码。但是请注意,如果您fn
使用字符串输入进行调用,则代码应该会检测到类型错误。与 C 不同,Lisp 中运行时的类型错误是明确定义的(只要声明的类型(默认为 T)包含运行时所有可能的值)。因此,为了安全而编译 Lisp 代码往往会在程序的多个点添加大量错误检查。
优化代码包括删除保证始终正确的检查,消除生成代码中的死分支。例如,如果你fn
单独考虑,你可以看到每次调用它时都必须检查它的输入,因为它很可能用字符串输入来调用。但是当您直接内联操作时,i
可以将索引静态确定为整数,这允许调用=
和rem
应用而无需(大量)错误检查。
SBCL 中的优化之所以发生,是因为有一个静态分析将变量映射到 Lisp 类型格的元素(and
并且or
基本上是类型的最大下限和最低上限,两端都有类型T
和类型)。nil
SBCL 只报告肯定会发生的错误:如果你调用一个接受从 0 到 5 的整数的函数,如果你用一个已知总是高于 5 或低于 0 的输入调用它,那么你就有一个错误(两个集合没有交集) ),但是如果使用 2 到 10 之间的整数调用它,则不会发出警告。这是安全的,因为编译器可以在运行时延迟错误检查,这与运行时没有类型感的其他语言相反(每次尝试警告代码可能考虑到 Lisp 的开放性,有错误会导致很多警告)。
您可以(declaim (inline fn))
在您的文件中再将性能与第一个版本相同。一个经验法则是,在函数内部,事情比在全局环境中更加静态:不能重新定义局部函数,可以精确定义局部变量的类型等。您可以更好地控制始终正确的内容。
请注意,如果执行很长时间(相对于其余代码),错误检查的开销是一个问题。(simple-array single-float)
如果您用单浮点数填充一个大数组并在其上应用数字代码,则使用专门的数组类型(如)或将局部变量声明为浮点数是有意义的(declare (type single-float x))
,这样您就不会检查每个值是否为有效地浮动。在其他情况下,开销不足以花费太多时间来减少它。
推荐阅读
- python - 在 python 中安装和导入模块的问题
- ruby-on-rails - Rails Activerecord:如何排除具有多个关联记录的记录
- javascript - 放大时热图单元格之间的白线(浏览器缩放)
- css - 如何使 ng-select 删除和只读
- hadoop - 纱线调度负载模拟器 - fair-scheduler
- android - 如何阻止 Android 捕获 KEYCODE_F2 和 KEY_CODE_F4?
- java - 动态元素数量布局问题
- image - 海量图片上传到mediawiki
- ios - iOS - 没有引发 Alamofire RequestRetrier
- spring - 使用 SameSite Cookie 禁用 Spring Security CSRF 保护