[ANN] proxy-plus: Faster and more usable replacement for "proxy"

257 views
Skip to first unread message

Nathan Marz

unread,
Jan 13, 2020, 11:47:06 AM1/13/20
to Clojure
proxy+ is a replacement for Clojure's proxy that's faster and more usable. proxy has a strange implementation where it overrides every possible method and uses a mutable field to store a map of string -> function for dispatching the methods. This causes it to be unable to handle methods with the same name but different arities.

proxy+ fixes these issues with proxy. Usage is like reify, and it's up to 10x faster.

John Newman

unread,
Jan 13, 2020, 12:29:35 PM1/13/20
to Clojure
Bravo 👏👏👏👏👏

Are there any differences in behavior to be aware of? AOT, Graal, consuming proxy+ classes from vanilla clojure classes?

--
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to clo...@googlegroups.com
Note that posts from new members are moderated - please be patient with your first post.
To unsubscribe from this group, send email to
clojure+u...@googlegroups.com
For more options, visit this group at
http://groups.google.com/group/clojure?hl=en
---
You received this message because you are subscribed to the Google Groups "Clojure" group.
To unsubscribe from this group and stop receiving emails from it, send an email to clojure+u...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/clojure/6d9bf48a-c5b5-417a-9f66-aa494cc38346%40googlegroups.com.

Nathan Marz

unread,
Jan 13, 2020, 1:28:46 PM1/13/20
to Clojure
No differences in behavior except for API being like reify. It integrates with AOT and can be consumed just like any other class. No idea how it interacts with Graal.


On Monday, January 13, 2020 at 12:29:35 PM UTC-5, John Newman wrote:
Bravo 👏👏👏👏👏

Are there any differences in behavior to be aware of? AOT, Graal, consuming proxy+ classes from vanilla clojure classes?

On Mon, Jan 13, 2020, 11:47 AM Nathan Marz <natha...@gmail.com> wrote:
proxy+ is a replacement for Clojure's proxy that's faster and more usable. proxy has a strange implementation where it overrides every possible method and uses a mutable field to store a map of string -> function for dispatching the methods. This causes it to be unable to handle methods with the same name but different arities.

proxy+ fixes these issues with proxy. Usage is like reify, and it's up to 10x faster.

--
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to clo...@googlegroups.com
Note that posts from new members are moderated - please be patient with your first post.
To unsubscribe from this group, send email to

For more options, visit this group at
http://groups.google.com/group/clojure?hl=en
---
You received this message because you are subscribed to the Google Groups "Clojure" group.
To unsubscribe from this group and stop receiving emails from it, send an email to clo...@googlegroups.com.

Brent Millare

unread,
Jan 14, 2020, 10:30:32 AM1/14/20
to Clojure
I skimmed the code, I don't really understand how it makes it faster over proxy. Is it the generated ASM is better? What's the in-a-nutshell description of how it works?

Nathan Marz

unread,
Jan 14, 2020, 11:58:17 AM1/14/20
to Clojure
The speedup comes from proxy+ directly overriding methods with the provided implementation, while Clojure's proxy has additional indirection. For example, if you do (proxy [Object] [] (toString [] "hello")), the bytecode for toString is:

  public java.lang.String toString();

     0  aload_0 [this]

     1  getfield user.proxy$java.lang.Object$ff19274a.__clojureFnMap : clojure.lang.IPersistentMap [16]

     4  ldc <String "toString"> [52]

     6  invokestatic clojure.lang.RT.get(java.lang.Object, java.lang.Object) : java.lang.Object [36]

     9  dup

    10  ifnull 28

    13  checkcast clojure.lang.IFn [38]

    16  aload_0 [this]

    17  invokeinterface clojure.lang.IFn.invoke(java.lang.Object) : java.lang.Object [55] [nargs: 2]

    22  checkcast java.lang.String [57]

    25  goto 33

    28  pop

    29  aload_0 [this]

    30  invokespecial java.lang.Object.toString() : java.lang.String [59]

    33  areturn


Clojure keeps the implementations in a map, and for every dispatch it does a map lookup by the method name. This is also why it can't handle overriding the same method name with different arities.

For (proxy+ [] Object (toString [this] "hello")), the bytecode is:

  public java.lang.String toString();

     0  aload_0 [this]

     1  getfield user.proxy_plus5358.toString5357 : clojure.lang.IFn [19]

     4  aload_0 [this]

     5  invokeinterface clojure.lang.IFn.invoke(java.lang.Object) : java.lang.Object [30] [nargs: 2]

    10  checkcast java.lang.String [32]

    13  areturn


The implementation function is stored as a field, so the cost of dispatch is a field get rather than a map lookup.

Clojure's proxy also overrides every available method in all superclasses/interfaces, while proxy+ only overrides what you specify. So proxy+ generates much smaller classes than proxy.

Brent Millare

unread,
Jan 14, 2020, 1:43:21 PM1/14/20
to Clojure
Thanks this is wonderful! Nice work

Mike Rodriguez

unread,
Jan 15, 2020, 11:46:36 AM1/15/20
to Clojure
Do you have any idea about the reason that the Clojure implementation was done this way - when it obviously seems a bit limited and also slower than necessary? Just curious if there's some historical context.


On Tuesday, January 14, 2020 at 11:58:17 AM UTC-5, Nathan Marz wrote:

Alex Miller

unread,
Jan 15, 2020, 12:31:13 PM1/15/20
to Clojure
Using vars lets you iterate on the impl functions without invalidating the proxy instances. I'm not sure if that was the reason, but that would be one advantage.

Mike Rodriguez

unread,
Jan 15, 2020, 12:48:42 PM1/15/20
to Clojure
Ah yes. I didn't consider that.

Ghadi Shayban

unread,
Jan 20, 2020, 11:57:31 AM1/20/20
to Clojure
I tried using proxy+ for one of the proxy implementations in clojure.core, but I ran into an issue where a "too many matching methods" got thrown at macroexpansion time. The proxy I tried to replicate was PrintWriter-on. The exception-data contained two "write" methods that matched arity but didn't match param types. Repro below.

(require '[com.rpl.proxy-plus :refer [proxy+]]))

(defn ^java.io.PrintWriter proxy+test
  [flush-fn close-fn]
  (let [sb (StringBuilder.)]
    (-> (proxy+ []
          java.io.Writer
          (flush [this]
                 (when (pos? (.length sb))
                   (flush-fn (.toString sb)))
                 (.setLength sb 0))
          (close [this]
                 (.flush this)
                 (when close-fn (close-fn))
                 nil)
          (write [this ^chars str-cbuf off len]
                 (when (pos? len)
                   (.append sb str-cbuf ^int off ^int len))))
        java.io.BufferedWriter.
        java.io.PrintWriter.)))

Nathan Marz

unread,
Jan 20, 2020, 6:31:52 PM1/20/20
to Clojure
Thanks. It doesn't currently use type hints to distinguish between multiple methods of the same arity. Opened an issue: https://github.com/redplanetlabs/proxy-plus/issues/1
Reply all
Reply to author
Forward
0 new messages