3
votes

For 2D graphics I need to optimize my functions, but in SBCL I get lots of comments about SBCL not being able to inline arithmetic operations. I tried all sorts of declarations but it doesn't seem to make the compiler happy. Here is a simple example:

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

gives the following 4 notes below. I'm completely lost as #'floor is a builtin function. I tried to find information/tutorials on how to properly give the compiler hints in SBCL and didn't find the right information, so any info would be very appreciated! Unfortunately optimization in Common Lisp is quite unknown territory for me. I'm using SBCL 1.3.20 on a Linux machine.

; 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 Answers

3
votes

When you call floor, you have to deal with different subtypes of numbers: the code divides a float by an integer (that may involve coercing the integer as a float), and then have to coerce the result back to an integer. This is the amount of work that must be done, correctly, and you are unlikely to bypass it if you don't restrict your input types.

If instead you use ffloor, then the primary result is a float (you can still round it to an integer later, when you really need it (e.g. convert to pixels coordinates)). The following code does not give compilation notes:

(defun test-floor (x div)
  (declare (type single-float x)
           (type fixnum div)
           (optimize (speed 3)))
  (ffloor x div))

You may even declare div as being a float, which would push the responsibility of providing appropriately typed values (and performing runtime checks) to the callers.

Note also that you should probably (declaim (inline test-floor)) before you define the function; that helps because then the compiler can put shortcuts in the code to avoid checking input parameter types and boxing results.

Edit:

The range of a float cover a large possible domain (due to the exponent): more densely packed near zero, more spaced towards infinity. Integer values are linearly spaced, but cover a smaller range with the same number of bits. So if you want to guarantee your output fits in a fixnum, you have to make sure the float in your inputs does not go outside the range of fixnum, too. I tried the following:

(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))

I had to half the upper range of the floats because when you test:

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

... it returns NIL. I don't have much time to see why this happens, but this depends on your implementation and architecture. Taking half the most positive fixnum ensures the value is low enough to be converted as a fixnum. Now, I have no more compilation notes.

(the same goes for (signed-byte 64)))

NB. Unlike in the above example, you should use deftype and avoid repeating the same declarations everywhere.

3
votes

If you want to specify the return value of an expression you can use THE:

(the fixnum (1+ 3))

But you really want to make sure that the value is actually a fixnum. If you 'lie', then Lisp may believe you and you have unspecified runtime effects. SBCL may warn at compile time, but you really should take care of that. If you provide wrong types are return wrong types, data may get corrupted and/or Lisp may crash.

Another way to specify a return value is the FTYPE declaration:

For example a function ith may take an integer and a list as arguments. It returns an arbitrary type -> T or any subtype.

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

For example:

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

There you need to be sure that:

  • a is an fixnum
  • b is an fixnum
  • the sum of a and b is also always a fixnum

Here this is easier, since the sum of a and b is definitely a fixnum:

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

Lisp may check that at runtime and/or compile-time. The addition now can be a simple fixnum operation and does not need to deal with fixnum overflows and bignums. If you set the safety value to something low, then the runtime check may also be omitted. But: you should never call this code with the wrong types.

1
votes

SBCL is claiming that it can’t optimise the call to floor because it isn’t sure the return value will be small enough to fit into a 64-bit integer.

CL-USER> (test-floor 1f25 1234)
8103727629894569426944 ;
0.0
CL-USER> (format nil “~b” *)
;; a long binary string 
CL-USER> (length *)
73

A 73 bit Integer may be returned but does not fit into 64 bits.

Se also the SBCL manual:

Edit: after some searching I have found the transformation for floor. It is here. I reproduce it below:

(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))))

So that explains what the compiler messages were talking about