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

snuglas via Sbcl-commits <[email protected]>
Newsgroups gmane.lisp.steel-bank.cvs
Message-ID <[email protected]>
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
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.