Optimization 如何说服Lisp SBCL执行内嵌fixnum算法?

Optimization 如何说服Lisp SBCL执行内嵌fixnum算法?,optimization,lisp,common-lisp,sbcl,fixnum,Optimization,Lisp,Common Lisp,Sbcl,Fixnum,我在其他SO答案中发现了一些技巧,但显然我无法说服SBCL执行内联fixnum算法: (declaim (optimize (speed 2) (safety 1))) (declaim (ftype (function (fixnum fixnum) double-float) fixnumtest) (inline fixnumtest)) (defun fixnumtest (i j) (declare (type fixnum i j)) (let* ((n (the fixn

我在其他SO答案中发现了一些技巧,但显然我无法说服SBCL执行内联fixnum算法:

(declaim (optimize (speed 2) (safety 1)))

(declaim (ftype (function (fixnum fixnum) double-float) fixnumtest) (inline fixnumtest))
(defun fixnumtest (i j)
  (declare (type fixnum i j))
  (let* ((n (the fixnum (+ i j)))
         (n+1 (the fixnum (1+ n))))
    (declare (type fixnum n n+1))
    (/ 1.0d0 (the fixnum (* n n+1)) )
  )
)

(defun main () 
  (format t "~11,9F~%" (fixnumtest 2 3))
) 
:导致
被迫执行通用-*(成本30)

我还应该尝试什么

$ sbcl --eval '(load (compile-file "play.lisp"))'
This is SBCL 1.5.1,
…
; compiling file "/opt/tmp/play.lisp" (written 16 OCT 2019 08:03:15 PM):
; compiling (DECLAIM (OPTIMIZE # ...))
; compiling (DECLAIM (FTYPE # ...) ...)
; compiling (DEFUN FIXNUMTEST ...)
; file: /opt/tmp/play.lisp
; in: DEFUN FIXNUMTEST
;     (* N N+1)
; 
; note: forced to do GENERIC-* (cost 30)
;       unable to do inline fixnum arithmetic (cost 4) because:
;       The result is a (VALUES
;                        (INTEGER -21267647932558653961849226946058125312
;                         21267647932558653961849226946058125312)
;                        &OPTIONAL), not a (VALUES FIXNUM &REST T).
;       unable to do inline (signed-byte 64) arithmetic (cost 5) because:
;       The result is a (VALUES
;                        (INTEGER -21267647932558653961849226946058125312
;                         21267647932558653961849226946058125312)
;                        &OPTIONAL), not a (VALUES (SIGNED-BYTE 64) &REST T).
;       etc.
另外,我认为执行浮点到指针强制(代价13)是从函数返回浮点的普通结果,这是正确的吗

;     (DEFUN FIXNUMTEST (I J)
;       (DECLARE (TYPE FIXNUM I J))
;       (LET* ((N (THE FIXNUM #)) (N+1 (THE FIXNUM #)))
;         (DECLARE (TYPE FIXNUM N N+1))
;         (/ 1.0d0 (THE FIXNUM (* N N+1)))))
; --> PROGN SB-IMPL::%DEFUN SB-IMPL::%DEFUN SB-INT:NAMED-LAMBDA 
; ==>
;   #'(SB-INT:NAMED-LAMBDA FIXNUMTEST
;         (I J)
;       (DECLARE (SB-C::TOP-LEVEL-FORM))
;       (DECLARE (TYPE FIXNUM I J))
;       (BLOCK FIXNUMTEST
;         (LET* ((N #) (N+1 #))
;           (DECLARE (TYPE FIXNUM N N+1))
;           (/ 1.0d0 (THE FIXNUM #)))))
; 
; note: doing float to pointer coercion (cost 13) to "<return value>"
;(除固定测试(I J)
;(声明(类型FIXNUM I J))
(让*((N(FIXNUM))(N+1(FIXNUM))
;(声明(键入FIXNUM N+1))
(/1.0d0(FIXNUM(*N+1(()))))
; --> 程序SB-IMPL::%DEFUN SB-IMPL::%DEFUN SB-INT:NAMED-LAMBDA
; ==>
;   #'(SB-INT:NAMED-LAMBDA fixmtest
(I J)
(声明(SB-C::顶级表单))
;(声明(类型FIXNUM I J))
(块固定测试)
(让*((N#)(N+1#))
;(声明(键入FIXNUM N+1))
(/1.0d0(FIXNUM##))
; 
; 注意:对“”执行浮点到指针强制(成本13)

好吧,编译器正在告诉你答案,也许是以一种稍微没有帮助的方式。如果您有两个fixnum,则情况并非如此,例如,添加它们会导致fixnum:类型
fixnum
在算术运算下不会关闭(甚至在
+
-
*
下也不会关闭,忽略
//code>)

从:

SBCL编译器处理类型声明的方式不同于大多数其他Lisp编译器。在默认编译策略下,编译器不会盲目相信类型声明,而是认为它们是关于应该检查的程序的断言:所有未被证明始终有效的类型声明都会在运行时断言

如果你想编译机器算术,你需要做的是告诉编译器它使用的类型足够好,它可以知道结果类型足够好,可以立即表示出来

根据函数中的算法,并假设采用64位实现,那么好的类型是
(带符号的字节31)
:使用
(带符号的字节32)
很有诱惑力,但这失败了,因为最终得到的是大于
(带符号的字节64)

因此,除了考虑返回时的最终双浮点数外,此代码不会发出警告:

(deftype smallish-integer (&optional (bits 31))
  `(signed-byte ,bits))


(declaim (ftype (function (smallish-integer smallish-integer) double-float)
                fixnumtest)
         (inline fixnumtest))

(defun fixnumtest (i j)
  (declare (optimize (speed 2)))
  (declare (type smallish-integer i j))
  (let* ((n (+ i j))
         (n+1 (1+ n)))
    (/ 1.0d0 (* n n+1))))
值得注意的是,
(有符号字节64)
fixnum
大很多:这是可以的,因为在一个函数中,编译器可以处理适合寄存器的数字,即使它们比fixnums大

我对x64汇编程序不太熟悉,无法检查所有的算法是否都编译为机器指令,但看起来确实如此


可以说服SBCL编译器,您不在乎得到正确的答案,它应该只执行机器运算,即使它知道可能会溢出。我不知道该怎么做。

似乎提供的答案tfb允许代码片段进一步缩减:

(declaim (optimize (speed 2)))

(deftype smallish-integer (&optional (bits 31))
  `(signed-byte ,bits))

(declaim (inline smallishtest))
(defun smallishtest (i j)
  (declare (type smallish-integer i j))
  (/ 1.0d0 (* (+ i j) (+ i j 1))))

(defun main () 
  (format t "~11,9F~%" (smallishtest 2 3))
)
(deftype smallish-integer (&optional (bits 31))
  `(signed-byte ,bits))

(declaim (inline smallishtest))
(defun smallishtest (i j)
  (declare (type smallish-integer i j))
  (/ 1.0d0 (* (+ i j) (+ i j 1))))

(defun main () 
  (format t "~11,9F~%" (smallishtest 2 3))
)
:并且仍然只给出一个编译注释:

; note: doing float to pointer coercion (cost 13) to "<return value>"

我在报纸上找到了一个好答案。 @tfb很好地描述了关键问题。算术运算可能超出范围

正常方式

解决此问题的第一种方法是声明生成的类型仍然是
fixnum
。但是,如果溢出,则结果未定义:

(defun add-e (x y)
  (declare (type (unsigned-byte 32) x y))
  (the (unsigned-byte 32) (+ x y)))
更好的方法

更好的方法是对结果使用位运算:

(defun add-d (x y)
  (declare (type (unsigned-byte 32) x y))
  (logand (+ x y) #xffffffff))
即使它溢出,结果仍然是您所期望的

现代编译器将对此进行优化,以确保结果在使用硬件表示的可接受范围内


这篇文章值得更详细地阅读。

对于足够大的N,溢出时会发生什么?与依赖于实现的fixnums不同,它通常有助于指定允许值的范围(实际上是fixnum的子集)并饱和或包裹任何溢出。此外,当变量已经声明为类型时,您使用“the”的操作也过了头。(fixnum的最小可移植大小是16位,但如果您精细地声明类型,则通常是针对特定的impl)>>应该发生什么>使用“the”过度使用“the”是否可以合理地说,我(显然还有其他人)误解了(fixnum(*n+1))之类的内容,意思是“只做机器算术”-然而SBCL理解,如果结果总是可以在结果类型中表示,那么就意味着执行算术运算?@igouy:是的,著名的CMUCL系列CLs将类型声明视为关于要检查的程序的断言,因此
(fixnum…)
的意思类似于“我声明,
..
的结果是一个
fixnum
,请检查它是(然后您可以在后面的类型推断中使用它的事实)”。我在答案中添加了对手册的引用("e)。看起来,如果
safety
0
,那么它可能只是假设类型就是您所说的类型0@igouy注意,不安全代码是不安全的。如果您的数字溢出,那么您不能假设算术将按照您可能期望的方式进行包装。这可能会发生,但也可能会导致系统死机,这取决于标记位发生了什么以及标记位在字中的位置。@tfb当任何算术运算可能溢出时,如何避免将每个操作都包装在
the
中?我认为是块编译(我认为SBCL现在又有了块编译)也可以这样做,而无需内联。但我还没有测试过。