emacs-devel
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Compiler macro for apply-partially


From: Adam Porter
Subject: Compiler macro for apply-partially
Date: Tue, 24 Aug 2021 01:37:47 -0500
User-agent: Gnus/5.13 (Gnus v5.13) Emacs/26.3 (gnu/linux)

Hi,

I wondered if using apply-partially had a significant runtime cost, so I
did a little benchmark using my bench-multi-lexical macro.[0]  I found
that it seems to be much slower than writing a lambda form directly,
which the byte-compiler can optimize more effectively.  It also conses,
so it can contribute to earlier GC.  Whether it has a noticable effect
depends on the application, I suppose.

Anyway, I came up with a simple compiler-macro declaration that seems to
help.  In these benchmarks, the version with the compiler-macro is named
`apply-partially*': it's roughly twice as fast as `apply-partially' and
produces about half as much garbage.

I was curious to see if it could be further optimized for cases in which
the partially applied function is called with only one argument, which
is the most common case for me, so I also made a version that does that,
here named `apply-partially**', and it's virtually identical in
performance to writing a lambda directly.

Please see the benchmark code and results below.  If any of these
enhancements would be useful, I'd be glad to submit a patch.  Please let
me know.

Thanks,
Adam

0: https://github.com/alphapapa/emacs-package-dev-handbook#bench-multi-lexical

#+BEGIN_SRC elisp :exports code :results silent
  (defun apply-partially* (fun &rest args)
    "Return a function that is a partial application of FUN to ARGS.
  ARGS is a list of the first N arguments to pass to FUN.  The
  result is a new function which does the same as FUN, except that
  the first N arguments are fixed at the values with which this
  function was called."
    (declare (compiler-macro (lambda (exp)
                               `(lambda (&rest args2)
                                  (apply ,fun ,@args args2)))))
    (lambda (&rest args2)
      (apply fun (append args args2))))

  (defun apply-partially** (fun &rest args)
    "Return a function that is a partial application of FUN to ARGS.
  ARGS is a list of the first N arguments to pass to FUN.  The
  result is a new function which does the same as FUN, except that
  the first N arguments are fixed at the values with which this
  function was called, and it only accepts one additional
  argument."
    (declare (compiler-macro (lambda (exp)
                               `(lambda (arg2)
                                  ,(pcase fun
                                     (`(function ,fun)
                                      `(,fun ,@args arg2))
                                     (_ `(,fun ,@args arg2)))))))
    (lambda (arg2)
      (apply fun (append args (list arg2)))))
#+END_SRC

* With one applied argument

#+BEGIN_SRC elisp :exports both
  (bench-multi-lexical :times 100000 :ensure-equal t
    :forms (("lambda" (let* ((foo 'FOO)
                             (fn (lambda (arg)
                                   (eq arg foo))))
                        (cl-loop for i below 100
                                 do (funcall fn foo))))
            ("apply-partially" (let* ((foo 'FOO)
                                      (fn (apply-partially #'eq foo)))
                                 (cl-loop for i below 100
                                          do (funcall fn foo))))
            ("apply-partially*" (let* ((foo 'FOO)
                                       (fn (apply-partially* #'eq foo)))
                                  (cl-loop for i below 100
                                           do (funcall fn foo))))
            ("apply-partially**" (let* ((foo 'FOO)
                                        (fn (apply-partially** #'eq foo)))
                                   (cl-loop for i below 100
                                            do (funcall fn foo))))))
#+END_SRC

#+RESULTS:
| Form              | x faster than next | Total runtime | # of GCs | Total GC 
runtime |
|-------------------+--------------------+---------------+----------+------------------|
| lambda            |               1.02 |      0.446226 |        0 |           
     0 |
| apply-partially** |               5.11 |      0.456534 |        0 |           
     0 |
| apply-partially*  |               1.99 |      2.331980 |        5 |         
1.476272 |
| apply-partially   |            slowest |      4.629640 |       11 |         
3.257624 |

* With two applied arguments

#+BEGIN_SRC elisp :exports both
  (bench-multi-lexical :times 100000 :ensure-equal t
    :forms (("lambda" (let* ((n 0)
                             (m 1)
                             (fn (lambda (x y z)
                                   (< x y z))))
                        (cl-loop for i below 100
                                 do (funcall fn n m i))))
            ("apply-partially" (let* ((n 0)
                                      (m 1)
                                      (fn (apply-partially #'< n m)))
                                 (cl-loop for i below 100
                                          do (funcall fn i))))
            ("apply-partially*" (let* ((n 0)
                                       (m 1)
                                       (fn (apply-partially* #'< n m)))
                                  (cl-loop for i below 100
                                           do (funcall fn i))))
            ("apply-partially**" (let* ((n 0)
                                        (m 1)
                                        (fn (apply-partially** #'< n m)))
                                   (cl-loop for i below 100
                                            do (funcall fn i))))))
#+END_SRC

#+RESULTS:
| Form              | x faster than next | Total runtime | # of GCs | Total GC 
runtime |
|-------------------+--------------------+---------------+----------+------------------|
| apply-partially** |               1.00 |      0.551158 |        0 |           
     0 |
| lambda            |               4.38 |      0.551878 |        0 |           
     0 |
| apply-partially*  |               2.75 |      2.415262 |        5 |         
1.485259 |
| apply-partially   |            slowest |      6.640960 |       17 |         
5.059900 |

* With two applied arguments and two called arguments

#+BEGIN_SRC elisp :exports both
  (bench-multi-lexical :times 100000 :ensure-equal t
    :forms (("lambda" (let* ((n 0)
                             (m 1)
                             (fn (lambda (x y z a)
                                   (< x y z a))))
                        (cl-loop for i below 100
                                 do (funcall fn n m i i))))
            ("apply-partially" (let* ((n 0)
                                      (m 1)
                                      (fn (apply-partially #'< n m)))
                                 (cl-loop for i below 100
                                          do (funcall fn i i))))
            ("apply-partially*" (let* ((n 0)
                                       (m 1)
                                       (fn (apply-partially* #'< n m)))
                                  (cl-loop for i below 100
                                           do (funcall fn i i))))))
#+END_SRC

#+RESULTS:
| Form             | x faster than next | Total runtime | # of GCs | Total GC 
runtime |
|------------------+--------------------+---------------+----------+------------------|
| lambda           |               7.06 |      0.629179 |        0 |            
    0 |
| apply-partially* |               1.87 |      4.441770 |       11 |         
3.310380 |
| apply-partially  |            slowest |      8.323220 |       22 |         
6.596604 |





reply via email to

[Prev in Thread] Current Thread [Next in Thread]