Common Lisp: 在SBCL中优化函数

3

针对2D图形,我需要优化函数,但是在SBCL中,我收到了很多有关SBCL无法内联算术操作的评论。我尝试过所有类型的声明,但似乎都不能使编译器满意。这里有一个简单的例子:

(defun test-floor (x div)
  (declare (type single-float x)
           (type (signed-byte 64) div)
           (optimize (speed 3)))
  (floor x div))

以下提供了4个笔记。我完全不理解#'floor是内置函数。我试图寻找有关如何在SBCL中正确给予编译器提示的信息/教程,但没有找到正确的信息。因此,任何信息都将非常感激!不幸的是,对于Common Lisp中的优化,我并不太熟悉。我正在Linux机器上使用SBCL 1.3.20。

; file: /tmp/file595dqU
; in: defun test-floor
;     (FLOOR CL-FLOCKS::X CL-FLOCKS::DIV)
; --> MULTIPLE-VALUE-BIND MULTIPLE-VALUE-CALL TRUNCATE LET* 
; ==>
;   (SB-KERNEL:%UNARY-TRUNCATE/SINGLE-FLOAT (/ SB-C::X SB-C::F))
; 
; note: forced to do full call
;       unable to do inline float truncate (cost 5) because:
;       The result is a (values integer &optional), not a (values
;                                                          (signed-byte 64) &rest
;                                                          t).

; --> MULTIPLE-VALUE-BIND MULTIPLE-VALUE-CALL TRUNCATE LET* VALUES - * 
; ==>
;   (SB-KERNEL:%SINGLE-FLOAT SB-C::RES)
; 
; note: forced to do full call
;       unable to do inline float coercion (cost 5) because:
;       The first argument is a integer, not a (signed-byte 64).

; --> MULTIPLE-VALUE-BIND MULTIPLE-VALUE-CALL FUNCTION IF VALUES 1- 
; ==>
;   (- SB-C::TRU 1)
; 
; note: forced to do generic-- (cost 10)
;       unable to do inline fixnum arithmetic (cost 1) because:
;       The first argument is a integer, not a fixnum.
;       The result is a (values integer &optional), not a (values fixnum &rest t).
;       unable to do inline fixnum arithmetic (cost 2) because:
;       The first argument is a integer, not a fixnum.
;       The result is a (values integer &optional), not a (values fixnum &rest t).
;       etc.

; --> MULTIPLE-VALUE-BIND MULTIPLE-VALUE-CALL FUNCTION IF VALUES 
; ==>
;   (+ REM SB-C::DIVISOR)
; 
; note: doing signed word to integer coercion (cost 20) from div, for:
;       the second argument of generic-+
; 
; compilation unit finished
;   printed 4 notes

CL-USER> 
3个回答

3
当你调用floor函数时,你需要处理不同子类型的数字:代码将一个浮点数除以一个整数(这可能需要将整数强制转换为浮点数),然后必须将结果强制转换回整数。这是必须正确完成的工作量,如果你不限制输入类型,你很难绕过它。
如果你使用ffloor函数,那么主要结果是一个浮点数(稍后仍然可以将其舍入为整数,当你真正需要它时(例如将其转换为像素坐标))。下面的代码不会产生编译提示:
(defun test-floor (x div)
  (declare (type single-float x)
           (type fixnum div)
           (optimize (speed 3)))
  (ffloor x div))

甚至可以将div声明为float,这样就将提供适当类型的值(并执行运行时检查)的责任推给了调用者。

还要注意,在定义函数之前,你应该可能使用(declaim (inline test-floor))。这有助于编译器在代码中放置快捷方式以避免检查输入参数类型和装箱结果。

注:

float的范围涵盖了一个大的可能域(由于指数):靠近零更密集,朝着无穷大更分散。整数值是线性间隔的,但用相同数量的比特覆盖较小的范围。因此,如果想要保证输出适合于fixnum,则必须确保您输入的浮点数不会超出fixnum的范围。我尝试了以下操作:

(defun test-round (x)
  (declare (type (single-float #.(float most-negative-fixnum 0f0)
                               #.(float (/ most-positive-fixnum 2) 0f0)) x)
           (optimize (speed 3)))
  (round x))

我不得不将浮点数的上限缩小一半,因为在测试时:

(typep (round (coerce most-positive-fixnum 'single-float)) 'fixnum)

当返回NIL时,我没有太多时间去看是为什么,但这取决于您的实现和架构。将最积极的fixnum除以2可确保该值足够低,可转换为fixnum。现在,我没有更多的编译注释。

(对于(signed-byte 64)也是同样的情况)

注意:与上面的示例不同,您应该使用deftype并避免在各处重复相同的声明。


谢谢,这真的很有帮助!正如你正确地假设的那样,我进行了大量像素计算,并且像素存储在数组中。但是我还没有找到一种方法可以在编译器不抱怨的情况下四舍五入浮点数:(defun test-round (x) (declare (type single-float x) (optimize (speed 3))) (round x)) - Orm Finnendahl
编译器抱怨结果是整数而不是(signed-byte 64)。如果我能以某种方式告诉编译器该值实际上64位范围内,那就太好了。 - Orm Finnendahl
1
问题是:你怎么知道结果在64位范围内?如果不是这种情况,应该发生什么?SBCL非常努力地防止您做错误的事情。您应该限制输入浮点域,以便结果必须在预期范围内(将进行编辑)。 - coredump
我在考虑限制输入浮点域,因为需要优化的函数只是在屏幕分辨率内进行向量数学运算,所以我可以安全地假设输入值是合理的。我甚至考虑使用固定数字和查找表来处理非有理操作,但我甚至不确定在当前CPU中是否更快速,因为FP单元存在。 - Orm Finnendahl

3

如果您想指定表达式的返回值,可以使用 THE

(the fixnum (1+ 3))

但是你真的需要确保这个值实际上是一个fixnum。如果你“说谎”,那么Lisp可能会相信你,并且你会有未指定的运行时效果。SBCL可能会在编译时发出警告,但你真的应该注意这个问题。如果你提供了错误的类型或返回了错误的类型,数据可能会损坏和/或Lisp可能会崩溃。

另一种指定返回值的方法是FTYPE声明:

例如,一个函数ith可以接受一个整数和一个列表作为参数。它返回一个任意类型->T或任何子类型。

(declaim (ftype (function (integer list) t)
                ith))

例如:

(the fixnum (+ (the fixnum a) (the fixnum b)))

在这里,你需要确保:

  • a 是一个 fixnum
  • b 是一个 fixnum
  • a 和 b 的和始终是一个 fixnum

这里更容易,因为 a 和 b 的和肯定是一个 fixnum:

CL-USER 3 > (let ((a 3) (b 12))
              (the fixnum (+ (the (integer 0 10) a)
                             (the (integer 3 20) b))))
15

Lisp可以在运行时和/或编译时检查这一点。现在的加法可以是一个简单的fixnum操作,不需要处理fixnum溢出和bignum。如果将safety值设置为低值,则可能会省略运行时检查。但是:您绝不能使用错误的类型调用此代码。


1
谢谢,非常有用!虽然原则上所有的都很清楚,但我不知道如何在代码中指定它,并且似乎很难找到关于这个话题的信息。所有这些评论肯定会帮助更好地理解CL / SBCL中的优化。感谢大家花时间帮助! - Orm Finnendahl

1
SBCL声称无法优化对floor的调用,因为它不确定返回值是否小到足以适应64位整数。
CL-USER> (test-floor 1f25 1234)
8103727629894569426944 ;
0.0
CL-USER> (format nil “~b” *)
;; a long binary string 
CL-USER> (length *)
73

可能返回一个73位整数,但无法适应64位。

请参阅SBCL手册:

编辑:经过一些搜索,我已经找到了floor的转换方式。在这里。我将其复制如下:

(deftransform floor ((number divisor))
  `(multiple-value-bind (tru rem) (truncate number divisor)
     (if (and (not (zerop rem))
              (if (minusp divisor)
                  (plusp number)
                  (minusp number)))
         (values (1- tru) (+ rem divisor))
         (values tru rem))))

那就解释了编译器信息所说的内容。

这看起来非常合理,谢谢!有没有办法告诉编译器结果永远不会超出64位范围?或者像coredump的答案建议的那样使用ffloor更有效率? - Orm Finnendahl
我还没有找到任何东西。 - Dan Robertson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接