[PATCH] Transform (+ base (* index {2, 4, 8})) to use LEA instruction

Anthony Green <[email protected]>
Newsgroups gmane.lisp.steel-bank.devel
Message-ID <CACxje58hpiXTroxrGuuJ0x7kEAt6u9j2PPQPZqcH+zQ+w3rNwA@mail.gmail.com>
Hi,

I'd like to submit a patch that adds IR1 transforms to recognize scaled
addition patterns and convert them to %LEA, generating a single LEA
instruction instead of separate multiply/shift and add operations.

Patterns matched:

  (+ base (* index scale))  where scale in {2, 4, 8}
  (+ base (ash index n))    where n in {1, 2, 3}

Both argument orderings are handled. The ash pattern is needed because
(* x 2) gets transformed to (ash x 1) by the power-of-2 multiply
optimization.

Before:

  (lambda (base index)
    (declare (fixnum base index))
    (+ base (* index 2)))

generated LEA + ADD (two instructions).

After:

  LEA RDX, [RAX+RDI*2]

(single instruction with scaled index addressing).

This was inspired by Matt Godbolt's "Addressing the Adding Situation"
blog post: https://xania.org/202512/02-adding-integers

Tests are included in x86-64-codegen.impure.lisp.

------------------------

Add IR1 transforms that recognize scaled addition patterns and convert
them to %LEA, generating a single LEA instruction instead of separate
multiply/shift and add operations.

Patterns matched:
  (+ base (* index scale))  where scale in {2, 4, 8}
  (+ base (ash index n))    where n in {1, 2, 3}

Both argument orderings are handled. The ash pattern is needed because
(* x 2) gets transformed to (ash x 1) by the power-of-2 multiply
optimization.

Inspired by Matt Godbolt's "Addressing the Adding Situation" blog post:
https://xania.org/202512/02-adding-integers
---
 src/compiler/x86-64/arith.lisp   | 71 ++++++++++++++++++++++++++++++++
 tests/x86-64-codegen.impure.lisp | 70 +++++++++++++++++++++++++++++++
 2 files changed, 141 insertions(+)

diff --git a/src/compiler/x86-64/arith.lisp b/src/compiler/x86-64/arith.lisp
index 2ebe22648..90127177d 100644
--- a/src/compiler/x86-64/arith.lisp
+++ b/src/compiler/x86-64/arith.lisp
@@ -3640,6 +3640,77 @@
   "recode as leas, shifts and adds"
   (*-transformer (lvar-value y) node 'sb-vm::%lea-modfx))

+;;; Transform (+ base (* index scale)) -> (%lea base index scale 0)
+;;; and       (+ base (ash index n))   -> (%lea base index 2^n 0)
+;;; where scale is 2, 4, or 8 (n is 1, 2, or 3).
+;;; This generates a single LEA instruction instead of SHL/IMUL + ADD.
+;;;
+;;; We match both * and ash because:
+;;; - (ash x n) is typically transformed to (* x 2^n) for n in {2,3}
+;;; - But (* x 2) gets transformed BACK to (ash x 1) by the generic
power-of-2 transform
+;;; See Matt Godbolt's "Addressing the Adding Situation" blog post:
+;;; https://xania.org/202512/02-adding-integers
+(defun try-scaled-add-lea-transform (scaled-arg scaled-is-second-arg node
lea-fun)
+  "Try to transform addition with scaled operand to LEA.
+   Returns the transform form or NIL if not applicable.
+   SCALED-IS-SECOND-ARG indicates position of the scaled arg in the +
call."
+  (let ((use (lvar-uses scaled-arg)))
+    (when (combination-p use)
+      (let ((fun-name (lvar-fun-name (combination-fun use))))
+        (cond
+          ;; Match (* index scale) where scale in {2, 4, 8}
+          ((eq fun-name '*)
+           (let* ((args (combination-args use))
+                  (scale-arg (and (= (length args) 2) (second args))))
+             (when (and scale-arg (constant-lvar-p scale-arg))
+               (let ((scale (lvar-value scale-arg)))
+                 (when (member scale '(2 4 8))
+                   (delay-ir1-transform node :constraint)
+                   (splice-fun-args scaled-arg '* 2)
+                   (if scaled-is-second-arg
+                       `(lambda (base index scale)
+                          (declare (ignore scale) (fixnum base index))
+                          (truly-the fixnum (,lea-fun base index ,scale
0)))
+                       `(lambda (index scale base)
+                          (declare (ignore scale) (fixnum base index))
+                          (truly-the fixnum (,lea-fun base index ,scale
0)))))))))
+          ;; Match (ash index n) where n in {1, 2, 3} -> scale in {2, 4, 8}
+          ;; This catches the (* x 2) -> (ash x 1) case
+          ((eq fun-name 'ash)
+           (let* ((args (combination-args use))
+                  (shift-arg (and (= (length args) 2) (second args))))
+             (when (and shift-arg (constant-lvar-p shift-arg))
+               (let ((shift (lvar-value shift-arg)))
+                 (when (member shift '(1 2 3))
+                   (let ((scale (ash 1 shift)))
+                     (delay-ir1-transform node :constraint)
+                     (splice-fun-args scaled-arg 'ash 2)
+                     (if scaled-is-second-arg
+                         `(lambda (base index shift)
+                            (declare (ignore shift) (fixnum base index))
+                            (truly-the fixnum (,lea-fun base index ,scale
0)))
+                         `(lambda (index shift base)
+                            (declare (ignore shift) (fixnum base index))
+                            (truly-the fixnum (,lea-fun base index ,scale
0)))))))))))))))
+
+;; Match fixnum + integer since (* fixnum const) produces wider integer
type
+(deftransform + ((x y) (fixnum integer) * :node node)
+  "use LEA for addition with scaled operand"
+  (or (try-scaled-add-lea-transform y t node '%lea)
+      (give-up-ir1-transform)))
+
+(deftransform + ((x y) (integer fixnum) * :node node)
+  "use LEA for addition with scaled operand"
+  (or (try-scaled-add-lea-transform x nil node '%lea)
+      (give-up-ir1-transform)))
+
+;; Also match fixnum + fixnum for ash case (which doesn't widen the type)
+(deftransform + ((x y) (fixnum fixnum) * :node node)
+  "use LEA for addition with shifted operand"
+  (or (try-scaled-add-lea-transform y t node '%lea)
+      (try-scaled-add-lea-transform x nil node '%lea)
+      (give-up-ir1-transform)))
+
 (defun exactly-one-read-p (results)
   (when results
     (let ((refs (tn-reads (tn-ref-tn results))))
diff --git a/tests/x86-64-codegen.impure.lisp
b/tests/x86-64-codegen.impure.lisp
index dbc2df515..7d2267ac9 100644
--- a/tests/x86-64-codegen.impure.lisp
+++ b/tests/x86-64-codegen.impure.lisp
@@ -1383,3 +1383,73 @@

 (with-test (:name :signed-vops) (test-signed))
 (with-test (:name :unsigned-vops) (test-unsigned))
+
+;;; Test that (+ base (* index {2,4,8})) and (+ base (ash index {1,2,3}))
+;;; compile to LEA instructions with scaled index addressing mode.
+;;; See Matt Godbolt's "Addressing the Adding Situation" blog post:
+;;; https://xania.org/202512/02-adding-integers
+(with-test (:name :scaled-add-uses-lea)
+  (flet ((has-scaled-lea-p (lambda-expr scale)
+           ;; Look for LEA with scaled index, e.g. "LEA RDX, [RAX+RDI*2]"
+           ;; We search for the scale pattern like "*2]" or "*4]" on a LEA
line
+           (let ((scale-pattern (format nil "*~d]" scale)))
+             (loop for line in (disassembly-lines (compile nil
lambda-expr))
+                   thereis (and (search "LEA" line)
+                                (search scale-pattern line)))))
+         (no-imul-p (lambda-expr)
+           (not (loop for line in (disassembly-lines (compile nil
lambda-expr))
+                      thereis (search "IMUL" line)))))
+    ;; Test (* index scale) forms with scales 2, 4, 8
+    (dolist (scale '(2 4 8))
+      (let ((expr `(lambda (base index)
+                     (declare (fixnum base index))
+                     (+ base (* index ,scale)))))
+        (assert (has-scaled-lea-p expr scale)
+                () "Expected LEA with scale ~d for (+ base (* index ~d))"
scale scale)
+        (assert (no-imul-p expr)
+                () "Unexpected IMUL for (+ base (* index ~d))" scale))
+      ;; Also test with arguments reversed: (+ (* index scale) base)
+      (let ((expr `(lambda (base index)
+                     (declare (fixnum base index))
+                     (+ (* index ,scale) base))))
+        (assert (has-scaled-lea-p expr scale)
+                () "Expected LEA with scale ~d for (+ (* index ~d) base)"
scale scale)))
+    ;; Test (ash index shift) forms with shifts 1, 2, 3 -> scales 2, 4, 8
+    (dolist (shift '(1 2 3))
+      (let ((scale (ash 1 shift))
+            (expr `(lambda (base index)
+                     (declare (fixnum base index))
+                     (+ base (ash index ,shift)))))
+        (assert (has-scaled-lea-p expr scale)
+                () "Expected LEA with scale ~d for (+ base (ash index
~d))" scale shift))
+      ;; Also test with arguments reversed
+      (let ((scale (ash 1 shift))
+            (expr `(lambda (base index)
+                     (declare (fixnum base index))
+                     (+ (ash index ,shift) base))))
+        (assert (has-scaled-lea-p expr scale)
+                () "Expected LEA with scale ~d for (+ (ash index ~d)
base)" scale shift)))))
+
+;;; Test correctness of scaled-add LEA optimization
+(with-test (:name :scaled-add-lea-correctness)
+  (let ((test-values '(-1000 -100 -10 -1 0 1 10 100 1000)))
+    ;; Test (* index scale) forms
+    (dolist (scale '(2 4 8))
+      (let ((f (compile nil `(lambda (base index)
+                               (declare (fixnum base index))
+                               (the fixnum (+ base (* index ,scale)))))))
+        (dolist (base test-values)
+          (dolist (index test-values)
+            (assert (= (funcall f base index)
+                       (+ base (* index scale)))
+                    () "Mismatch for (+ ~d (* ~d ~d))" base index
scale)))))
+    ;; Test (ash index shift) forms
+    (dolist (shift '(1 2 3))
+      (let ((f (compile nil `(lambda (base index)
+                               (declare (fixnum base index))
+                               (the fixnum (+ base (ash index ,shift)))))))
+        (dolist (base test-values)
+          (dolist (index test-values)
+            (assert (= (funcall f base index)
+                       (+ base (ash index shift)))
+                    () "Mismatch for (+ ~d (ash ~d ~d))" base index
shift)))))))
--
2.51.2

_______________________________________________
Sbcl-devel mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/sbcl-devel
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.