]> git.eshelyaron.com Git - emacs.git/commitdiff
Better compilation of n-ary comparisons
authorMattias Engdegård <mattiase@acm.org>
Sat, 28 Jan 2023 15:26:37 +0000 (16:26 +0100)
committerMattias Engdegård <mattiase@acm.org>
Sun, 29 Jan 2023 11:02:57 +0000 (12:02 +0100)
Transform n-ary comparisons to a chain of binary comparisons in the
Lisp optimiser instead of in codegen, to allow for subsequent
optimisations.  This generalises the transform, so that

   (< 1 X 10)  ->  (let ((x X)) (and (< 1 x) (< x 10)))

where (< 1 x) is then flipped to (> x 1) in codegen since it's
slightly more efficient to have the constant argument last.  Arguments
that are neither constants nor variables are given temporary bindings.

This results in about 2× speedup for 3-ary comparisons of fixnums with
nontrivial arguments, and also improves the code slightly for binary
comparisons with a constant first argument.

* lisp/emacs-lisp/byte-opt.el (byte-opt--nary-comparison): New,
set as the `byte-optimizer` property for =, <, <=, >, and >=.
* lisp/emacs-lisp/bytecomp.el (byte-compile-and-folded):
Rename to...
(byte-compile-cmp): ...and rewrite.

lisp/emacs-lisp/byte-opt.el
lisp/emacs-lisp/bytecomp.el

index b1a46d520e66a7f4d629c3f68b7d1686bfc603c3..4d39e28fc8ebbbcbdf08275460fca7ead3bffafa 100644 (file)
@@ -975,6 +975,43 @@ for speeding up processing.")
    (t ;; Moving the constant to the end can enable some lapcode optimizations.
     (list (car form) (nth 2 form) (nth 1 form)))))
 
+(defun byte-opt--nary-comparison (form)
+  "Optimise n-ary comparisons such as `=', `<' etc."
+  (let ((nargs (length (cdr form))))
+    (cond
+     ((= nargs 1)
+      `(progn (cadr form) t))
+     ((>= nargs 3)
+      ;; At least 3 arguments: transform to N-1 binary comparisons,
+      ;; since those have their own byte-ops which are particularly
+      ;; fast for fixnums.
+      (let* ((op (car form))
+             (bindings nil)
+             (rev-args nil))
+        (if (memq nil (mapcar #'macroexp-copyable-p (cddr form)))
+            ;; At least one arg beyond the first is non-constant non-variable:
+            ;; create temporaries for all args to guard against side-effects.
+            ;; The optimiser will eliminate trivial bindings later.
+            (let ((i 1))
+              (dolist (arg (cdr form))
+                (let ((var (make-symbol (format "arg%d" i))))
+                  (push var rev-args)
+                  (push (list var arg) bindings)
+                  (setq i (1+ i)))))
+          ;; All args beyond the first are copyable: no temporary variables
+          ;; required.
+          (setq rev-args (reverse (cdr form))))
+        (let ((prev (car rev-args))
+              (exprs nil))
+          (dolist (arg (cdr rev-args))
+            (push (list op arg prev) exprs)
+            (setq prev arg))
+          (let ((and-expr (cons 'and exprs)))
+            (if bindings
+                (list 'let (nreverse bindings) and-expr)
+              and-expr)))))
+     (t form))))
+
 (defun byte-optimize-constant-args (form)
   (let ((ok t)
        (rest (cdr form)))
@@ -1130,13 +1167,18 @@ See Info node `(elisp) Integer Basics'."
 (put 'max 'byte-optimizer #'byte-optimize-min-max)
 (put 'min 'byte-optimizer #'byte-optimize-min-max)
 
-(put '=   'byte-optimizer #'byte-optimize-binary-predicate)
 (put 'eq  'byte-optimizer #'byte-optimize-eq)
 (put 'eql   'byte-optimizer #'byte-optimize-equal)
 (put 'equal 'byte-optimizer #'byte-optimize-equal)
 (put 'string= 'byte-optimizer #'byte-optimize-binary-predicate)
 (put 'string-equal 'byte-optimizer #'byte-optimize-binary-predicate)
 
+(put '=  'byte-optimizer #'byte-opt--nary-comparison)
+(put '<  'byte-optimizer #'byte-opt--nary-comparison)
+(put '<= 'byte-optimizer #'byte-opt--nary-comparison)
+(put '>  'byte-optimizer #'byte-opt--nary-comparison)
+(put '>= 'byte-optimizer #'byte-opt--nary-comparison)
+
 (put 'string-greaterp 'byte-optimizer #'byte-optimize-string-greaterp)
 (put 'string> 'byte-optimizer #'byte-optimize-string-greaterp)
 
index aa9521e5a65422db6e2d47bcc8c55f729d099212..bfb9be4712bc89ef4a6cfae32fb51a198e7e7053 100644 (file)
@@ -3748,7 +3748,7 @@ If it is nil, then the handler is \"byte-compile-SYMBOL.\""
                                      '((0 . byte-compile-no-args)
                                        (1 . byte-compile-one-arg)
                                        (2 . byte-compile-two-args)
-                                       (2-and . byte-compile-and-folded)
+                                        (2-cmp . byte-compile-cmp)
                                        (3 . byte-compile-three-args)
                                        (0-1 . byte-compile-zero-or-one-arg)
                                        (1-2 . byte-compile-one-or-two-args)
@@ -3827,11 +3827,11 @@ If it is nil, then the handler is \"byte-compile-SYMBOL.\""
 (byte-defop-compiler cons              2)
 (byte-defop-compiler aref              2)
 (byte-defop-compiler set               2)
-(byte-defop-compiler (= byte-eqlsign)  2-and)
-(byte-defop-compiler (< byte-lss)      2-and)
-(byte-defop-compiler (> byte-gtr)      2-and)
-(byte-defop-compiler (<= byte-leq)     2-and)
-(byte-defop-compiler (>= byte-geq)     2-and)
+(byte-defop-compiler (= byte-eqlsign)  2-cmp)
+(byte-defop-compiler (< byte-lss)      2-cmp)
+(byte-defop-compiler (> byte-gtr)      2-cmp)
+(byte-defop-compiler (<= byte-leq)     2-cmp)
+(byte-defop-compiler (>= byte-geq)     2-cmp)
 (byte-defop-compiler get               2)
 (byte-defop-compiler nth               2)
 (byte-defop-compiler substring         1-3)
@@ -3895,18 +3895,20 @@ If it is nil, then the handler is \"byte-compile-SYMBOL.\""
     (byte-compile-form (nth 2 form))
     (byte-compile-out (get (car form) 'byte-opcode) 0)))
 
-(defun byte-compile-and-folded (form)
-  "Compile calls to functions like `<='.
-These implicitly `and' together a bunch of two-arg bytecodes."
-  (let ((l (length form)))
-    (cond
-     ((< l 3) (byte-compile-form `(progn ,(nth 1 form) t)))
-     ((= l 3) (byte-compile-two-args form))
-     ;; Don't use `cl-every' here (see comment where we require cl-lib).
-     ((not (memq nil (mapcar #'macroexp-copyable-p (nthcdr 2 form))))
-      (byte-compile-form `(and (,(car form) ,(nth 1 form) ,(nth 2 form))
-                              (,(car form) ,@(nthcdr 2 form)))))
-     (t (byte-compile-normal-call form)))))
+(defun byte-compile-cmp (form)
+  "Compile calls to numeric comparisons such as `<', `=' etc."
+  ;; Lisp-level transforms should already have reduced valid calls to 2 args.
+  (if (not (= (length form) 3))
+      (byte-compile-subr-wrong-args form "1 or more")
+    (byte-compile-two-args
+     (if (macroexp-const-p (nth 1 form))
+         ;; First argument is constant: flip it so that the constant
+         ;; is last, which may allow more lapcode optimisations.
+         (let* ((op (car form))
+                (flipped-op (cdr (assq op '((< . >) (<= . >=)
+                                            (> . <) (>= . <=) (= . =))))))
+           (list flipped-op (nth 2 form) (nth 1 form)))
+       form))))
 
 (defun byte-compile-three-args (form)
   (if (not (= (length form) 4))