[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 |
- Compiler macro for apply-partially,
Adam Porter <=