Re: master: x86-64: TRUNCATE using the Lemire, Kaser, Kurz transform

Stas Boukarev <[email protected]>
Newsgroups gmane.lisp.steel-bank.cvs,gmane.lisp.steel-bank.devel
Message-ID <CAF63=11GV4rpyE4bm6fQ2s7gmkOcSz+pqgv6PnS0DhKRZ9QoEg@mail.gmail.com>
The VOP gets defeated by one of the 23 transforms, when (speed 3):
https://github.com/sbcl/sbcl/blob/master/src/compiler/srctran.lisp#L5135

On Sun, Apr 19, 2026 at 5:18 AM snuglas via Sbcl-commits
<[email protected]> wrote:
>
> The branch "master" has been updated in SBCL:
>        via  ae89d7be84002e48bea7cfd8be84bd6c7896a2c0 (commit)
>       from  c41011cb52eb089b3edf219a871224afcfb8a122 (commit)
>
> - Log -----------------------------------------------------------------
> commit ae89d7be84002e48bea7cfd8be84bd6c7896a2c0
> Author: Douglas Katzman <[email protected]>
> Date:   Sat Apr 18 22:02:34 2026 -0400
>
>     x86-64: TRUNCATE using the Lemire, Kaser, Kurz transform
>
>     2 MULs outperforms one DIV or IDIV by at least 2x based on my
>     testing and also Agner Fog's instruction latency tables
> ---
>  src/code/target-package.lisp    |  2 +-
>  src/compiler/generic/utils.lisp |  8 +++++-
>  src/compiler/x86-64/arith.lisp  | 56 ++++++++++++++++++++++++++++++++++-------
>  tests/integerdiv.pure.lisp      | 18 +++++++++++++
>  4 files changed, 73 insertions(+), 11 deletions(-)
>
> diff --git a/src/code/target-package.lisp b/src/code/target-package.lisp
> index 1fc56ea2f..97135bea5 100644
> --- a/src/code/target-package.lisp
> +++ b/src/code/target-package.lisp
> @@ -439,7 +439,7 @@ of :INHERITED :EXTERNAL :INTERNAL."
>           (n min-numerator-bits))
>      (loop
>       (multiple-value-bind (c frac-bits)
> -         (sb-c:compute-fastrem-coefficient denominator n :variable)
> +         (sb-c:compute-fastrem-coefficient denominator n :minimum)
>         (cond ((= frac-bits 32)
>                (return (values (ldb (byte n 0) -1) c)))
>               ((> frac-bits 32)
> diff --git a/src/compiler/generic/utils.lisp b/src/compiler/generic/utils.lisp
> index 4a8a39cc1..85e6f5533 100644
> --- a/src/compiler/generic/utils.lisp
> +++ b/src/compiler/generic/utils.lisp
> @@ -577,7 +577,7 @@
>                         (when (<= d (+ (mod 2^F d) (expt 2 L)))
>                           (let ((c (ceiling (expt 2 F) d)))
>                             (return (values F c))))))))
> -    (cond ((eq fraction-bits :variable) ; return the smallest F
> +    (cond ((eq fraction-bits :minimum) ; return the smallest F
>             (values c smallest-f))
>            (t
>             ;; Otherwise hardwire F to 32 so the algorithm can use :DWORD
> @@ -589,6 +589,12 @@
>                      smallest-f d n))
>             (values (ceiling (expt 2 fraction-bits) d) fraction-bits)))))
>
> +(defun type-width-in-bits (ctype)
> +  ;; Return (integer-length upper-bound) of numeric type. This is NOT a theoretical
> +  ;; smallest N bits needed to encode an element of type in a packed representation
> +  ;; (e.g. the interval 5..8 _could_ be stored in 2 bits) but we don't do that.
> +  (integer-length (sb-c::interval-high (sb-c::numeric-type->interval ctype))))
> +
>  (defun env-system-tlab-p (env)
>    #-system-tlabs (declare (ignore env))
>    #+system-tlabs
> diff --git a/src/compiler/x86-64/arith.lisp b/src/compiler/x86-64/arith.lisp
> index 31ab987a4..2b5b9de99 100644
> --- a/src/compiler/x86-64/arith.lisp
> +++ b/src/compiler/x86-64/arith.lisp
> @@ -2056,13 +2056,13 @@
>
>  (define-vop (fast-truncate-c/fixnum=>fixnum fast-safe-arith-op)
>    (:translate truncate)
> -  (:args (x :scs (any-reg) :target eax))
> +  (:args (x :scs (any-reg) :target rax))
>    (:info y)
>    (:arg-types tagged-num (:constant fixnum))
>    (:temporary (:sc signed-reg :offset rax-offset :target quo
> -                   :from :argument :to (:result 0)) eax)
> +                   :from :argument :to (:result 0)) rax)
>    (:temporary (:sc any-reg :offset rdx-offset :target rem
> -                   :from :eval :to (:result 1)) edx)
> +                   :from :eval :to (:result 1)) rdx)
>    (:temporary (:sc any-reg :from :eval :to :result) y-arg)
>    (:results (quo :scs (any-reg))
>              (rem :scs (any-reg)))
> @@ -2070,16 +2070,54 @@
>    (:note "inline fixnum arithmetic")
>    (:vop-var vop)
>    (:generator 30
> -    (move eax x)
> +   (block nil
> +    ;; This ought to be done machine-independently, but there are already 23 transforms
> +    ;; on TRUNCATE and I don't understand how they get tried in the optimal order.
> +    ;; The benefit of that is we could just invoke the fastrem-32 or fastrem-64 vop
> +    ;; rather than having to replicate them inside here.
> +    (when (and (not (sb-c::tn-reads quo))
> +               (typep y '(unsigned-byte 64))
> +               (csubtypep (tn-ref-type (vop-args vop)) (specifier-type '(unsigned-byte 64))))
> +      ;; The reason for the concern about inputs is that the general algorithm to accept
> +      ;; any 64-bit integer (which as FIXNUM would limit be 62 bits) needs 4 MUL instructions
> +      ;; (confirmed by looking at https://github.com/lemire/fastmod)
> +      ;; But 2 MUL instructions is enough for reasonable inputs. Both the divisor and the
> +      ;; maximum input affect how many bits are needed in intermediate results.
> +      (binding* ((numerator-precision
> +                  (type-width-in-bits (tn-ref-type (vop-args vop))) :exit-if-null)
> +                 ((magic smallest-nbits)
> +                  (sb-c:compute-fastrem-coefficient y numerator-precision :minimum))
> +                 ((frac-bits operand-size)
> +                  ;; "smallest" is how many bits you need for the algorithm to work, assuming
> +                  ;; you could actually calculate on bit fields of arbitrary size. But to make it
> +                  ;; efficient, you need a fixed-binary-point reciprocal in exactly 32 bits
> +                  ;; or exactly 64 bits (or 128, if fully supporting 64-bit ints)
> +                  (cond ((<= smallest-nbits 32) (values 32 :dword))
> +                        ((<= smallest-nbits 64) (values 64 :qword)))))
> +        (when operand-size
> +          (when (/= smallest-nbits frac-bits) ; recompute MAGIC to required number of bits
> +            (setq magic (sb-c:compute-fastrem-coefficient y numerator-precision frac-bits)))
> +          (let ((arg-size (if (<= numerator-precision 31) :dword :qword)))
> +            (move rax x arg-size)
> +            (inst shr arg-size rax n-fixnum-tag-bits)) ; untag it
> +          (inst mul operand-size (register-inline-constant operand-size magic))
> +          (inst mul operand-size (register-inline-constant operand-size y))
> +          ;; The result might fit into a :DWORD depending on the divisor
> +          (let ((res-size (if (<= (integer-length (1- y)) 31) :dword :qword))) ; (tagged)
> +            (if (location= rem rdx)
> +                (inst shl res-size rem 1)
> +                (inst lea res-size rem (ea rdx rdx))))
> +          (return))))
> +    (move rax x)
>      (inst cqo)
>      (inst mov y-arg (fixnumize y))
>      (inst idiv y-arg)
> -    (if (location= quo eax)
> -        (inst shl eax n-fixnum-tag-bits)
> +    (if (location= quo rax)
> +        (inst shl rax n-fixnum-tag-bits)
>          (if (= n-fixnum-tag-bits 1)
> -            (inst lea quo (ea eax eax))
> -            (inst lea quo (ea nil eax (ash 1 n-fixnum-tag-bits)))))
> -    (move rem edx)))
> +            (inst lea quo (ea rax rax))
> +            (inst lea quo (ea nil rax (ash 1 n-fixnum-tag-bits)))))
> +    (move rem rdx))))
>
>  (define-vop (fast-truncate/unsigned=>unsigned fast-safe-arith-op)
>    (:translate truncate)
> diff --git a/tests/integerdiv.pure.lisp b/tests/integerdiv.pure.lisp
> index a9ca38873..e28763756 100644
> --- a/tests/integerdiv.pure.lisp
> +++ b/tests/integerdiv.pure.lisp
> @@ -150,3 +150,21 @@
>    (loop for divisor from 3 to 10000
>          do (format t "divisor=~d~%" divisor)
>              (try-fastrem divisor 18)))
> +
> +(declaim (ftype function remN))
> +#+x86-64 ; test the TRUNCATE -> fastrem optimization
> +(with-test (:name :test-rem-transform)
> +  (dolist (dividend-bits '(24 58))
> +    ;; a divisor of 1235 needs too many intermediate bits for dividend-bits=58
> +    (dolist (divisor `(3 7 133 ,(if (= dividend-bits 24) 1235 149)))
> +      (compile 'remN `(lambda (x) (rem (the (unsigned-byte ,dividend-bits) x) ,divisor)))
> +      ;; should not use DIV or IDIV instructions
> +      (assert (not (search "DIV" (with-output-to-string (s) (disassemble 'remN :stream s)))))
> +      (dotimes (i 10000)
> +        (declare (notinline floor))
> +        (assert (= (remn i) (nth-value 1 (floor i divisor)))))
> +      (let ((max (ash 1 dividend-bits)))
> +        (dotimes (i 10000)
> +          (let ((x (random max)))
> +            (declare (notinline floor))
> +            (assert (= (remn x) (nth-value 1 (floor x divisor))))))))))
>
> -----------------------------------------------------------------------
>
>
> hooks/post-receive
> --
> SBCL
>
>
> _______________________________________________
> Sbcl-commits mailing list
> [email protected]
> https://lists.sourceforge.net/lists/listinfo/sbcl-commits


_______________________________________________
Sbcl-commits mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/sbcl-commits
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.