Optimization 如何说服Lisp SBCL执行内嵌fixnum算法?
我在其他SO答案中发现了一些技巧,但显然我无法说服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
(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现在又有了块编译)也可以这样做,而无需内联。但我还没有测试过。