From mboxrd@z Thu Jan 1 00:00:00 1970 Path: news.gmane.io!.POSTED.blaine.gmane.org!not-for-mail From: Adam Porter Newsgroups: gmane.emacs.devel Subject: Compiler macro for apply-partially Date: Tue, 24 Aug 2021 01:37:47 -0500 Message-ID: <87a6l74abo.fsf@alphapapa.net> Mime-Version: 1.0 Content-Type: text/plain Injection-Info: ciao.gmane.io; posting-host="blaine.gmane.org:116.202.254.214"; logging-data="14116"; mail-complaints-to="usenet@ciao.gmane.io" User-Agent: Gnus/5.13 (Gnus v5.13) Emacs/26.3 (gnu/linux) To: emacs-devel@gnu.org Original-X-From: emacs-devel-bounces+ged-emacs-devel=m.gmane-mx.org@gnu.org Tue Aug 24 08:39:02 2021 Return-path: Envelope-to: ged-emacs-devel@m.gmane-mx.org Original-Received: from lists.gnu.org ([209.51.188.17]) by ciao.gmane.io with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.92) (envelope-from ) id 1mIQ5O-0003RA-1l for ged-emacs-devel@m.gmane-mx.org; Tue, 24 Aug 2021 08:39:02 +0200 Original-Received: from localhost ([::1]:46482 helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1mIQ5L-0004hG-UG for ged-emacs-devel@m.gmane-mx.org; Tue, 24 Aug 2021 02:38:59 -0400 Original-Received: from eggs.gnu.org ([2001:470:142:3::10]:50966) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1mIQ4R-00037H-4q for emacs-devel@gnu.org; Tue, 24 Aug 2021 02:38:03 -0400 Original-Received: from ciao.gmane.io ([116.202.254.214]:42252) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1mIQ4O-0002Zr-Vo for emacs-devel@gnu.org; Tue, 24 Aug 2021 02:38:02 -0400 Original-Received: from list by ciao.gmane.io with local (Exim 4.92) (envelope-from ) id 1mIQ4K-00029M-F7 for emacs-devel@gnu.org; Tue, 24 Aug 2021 08:37:56 +0200 X-Injected-Via-Gmane: http://gmane.org/ Received-SPF: pass client-ip=116.202.254.214; envelope-from=ged-emacs-devel@m.gmane-mx.org; helo=ciao.gmane.io X-Spam_score_int: -15 X-Spam_score: -1.6 X-Spam_bar: - X-Spam_report: (-1.6 / 5.0 requ) BAYES_00=-1.9, HEADER_FROM_DIFFERENT_DOMAINS=0.25, SPF_HELO_NONE=0.001, SPF_PASS=-0.001 autolearn=no autolearn_force=no X-Spam_action: no action X-BeenThere: emacs-devel@gnu.org X-Mailman-Version: 2.1.23 Precedence: list List-Id: "Emacs development discussions." List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: emacs-devel-bounces+ged-emacs-devel=m.gmane-mx.org@gnu.org Original-Sender: "Emacs-devel" Xref: news.gmane.io gmane.emacs.devel:272889 Archived-At: 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 |