-
-
Notifications
You must be signed in to change notification settings - Fork 19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Replace Clojure functions that contain metadata with custom FnWithMeta #150
Conversation
8a1391b
to
38faa21
Compare
fefe427
to
02b8b3b
Compare
@bshepherdson I've taken your commit from #149 and sorted out the tests for it to pass and then applied my changes on top. Together, this achieves good improvements in Metabase, the overhead from Methodical is mostly gone. |
src/methodical/FnWithMeta.java
Outdated
|
||
import clojure.lang.*; | ||
|
||
public class FnWithMeta extends AFunction { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm really wary of adding a Java class here since adding ClojureScript support to Methodical is definitely something I want to pursue at some point in the future. Couldn't you just create a custom deftype
that implements clojure.lang.IFn
and clojure.lang.IObject
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem is RestFn does more than that. It also implements a bunch of doInvoke
s and helpers to make apply
less expensive if the number of required arguments for the function is bigger than the number of supplied values. Rewriting that full class to a deftype would be daunting to say the least.
When Clojurescript support becomes desirable, you should be able to refactor the new functions (fn-with-meta, vary-fn-with-meta, unwrap) to defer to the plain with-meta
for fn objects in ClojureScript, using reader conditionals.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And proxy
is very bad performance-wise.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it really be that much more complicated than the other places we're already doing it, like here?
methodical/src/methodical/impl/standard.clj
Lines 187 to 233 in 274d0ef
clojure.lang.IFn | |
(invoke [_] | |
(invoke-multifn impl mta)) | |
(invoke [_ a] | |
(invoke-multifn impl mta a)) | |
(invoke [_ a b] | |
(invoke-multifn impl mta a b)) | |
(invoke [_ a b c] | |
(invoke-multifn impl mta a b c)) | |
(invoke [_ a b c d] | |
(invoke-multifn impl mta a b c d)) | |
(invoke [_ a b c d e] | |
(invoke-multifn impl mta a b c d e)) | |
(invoke [_ a b c d e f] | |
(invoke-multifn impl mta a b c d e f)) | |
(invoke [_ a b c d e f g] | |
(invoke-multifn impl mta a b c d e f g)) | |
(invoke [_ a b c d e f g h] | |
(invoke-multifn impl mta a b c d e f g h)) | |
(invoke [_ a b c d e f g h i] | |
(invoke-multifn impl mta a b c d e f g h i)) | |
(invoke [_ a b c d e f g h i j] | |
(invoke-multifn impl mta a b c d e f g h i j)) | |
(invoke [_ a b c d e f g h i j k] | |
(invoke-multifn impl mta a b c d e f g h i j k)) | |
(invoke [_ a b c d e f g h i j k l] | |
(invoke-multifn impl mta a b c d e f g h i j k l)) | |
(invoke [_ a b c d e f g h i j k l m] | |
(invoke-multifn impl mta a b c d e f g h i j k l m)) | |
(invoke [_ a b c d e f g h i j k l m n] | |
(invoke-multifn impl mta a b c d e f g h i j k l m n)) | |
(invoke [_ a b c d e f g h i j k l m n o] | |
(invoke-multifn impl mta a b c d e f g h i j k l m n o)) | |
(invoke [_ a b c d e f g h i j k l m n o p] | |
(invoke-multifn impl mta a b c d e f g h i j k l m n o p)) | |
(invoke [_ a b c d e f g h i j k l m n o p q] | |
(invoke-multifn impl mta a b c d e f g h i j k l m n o p q)) | |
(invoke [_ a b c d e f g h i j k l m n o p q r] | |
(invoke-multifn impl mta a b c d e f g h i j k l m n o p q r)) | |
(invoke [_ a b c d e f g h i j k l m n o p q r s] | |
(invoke-multifn impl mta a b c d e f g h i j k l m n o p q r s)) | |
(invoke [_ a b c d e f g h i j k l m n o p q r s t] | |
(invoke-multifn impl mta a b c d e f g h i j k l m n o p q r s t)) | |
(invoke [_ a b c d e f g h i j k l m n o p q r s t args] | |
(apply invoke-multifn impl mta a b c d e f g h i j k l m n o p q r s t args)) | |
(applyTo [_ args] | |
(apply invoke-multifn impl mta args)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That class is 3k lines of code. I guess with macros it can be mostly generated. I can try to reverse engineer the full details of what RestFn is doing and write such a macro.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I noticed only now that I extend AFunction, not RestFn. So, perhaps, it is indeed doable.
There's a lot of `apply`/`RestFn`/`invoke` etc. dynamic call machinery in Methodical's stack traces. This is an attempt to remove some of it by going up to 7 direct args for multimethod calls. (And dispatch functions.) This hasn't removed much of the `apply` overhead in practice because `with-meta` on a function wraps it with a naive function subclass that always does a dynamic call. There are probably still some places that more dynamic calls are creeping in, but I ran out of time to dig deeper. This may not go anywhere until I get back, but I wanted to publish this just in case.
f6efe28
to
cc8ad53
Compare
cc8ad53
to
0d75db1
Compare
Turned out it wasn't as much work as I thought it was. I've updated the PR, no more Java classes now. |
(:require [methodical.util :as u])) | ||
|
||
(defn partial* | ||
"[[clojure.core/partial]] but with more direct arities." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be nice to add a Kondo :discouraged-var
rule for clojure.core/partial
but I can do that separately
(with-meta (partial primary-method next-method) (meta primary-method))) | ||
(u/fn-with-meta (partial* (u/unwrap-fn-with-meta primary-method) next-method) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if maybe we should also have a with-meta*
that does the right thing for functions but otherwise does normal with-meta
and then have Kondo :discouraged-var
tell everyone to use that. Then we don't need fn-with-meta
and it's impossible to forget to use it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea!
@@ -60,9 +60,20 @@ | |||
([^MultiFnImpl impl mta a b c d] | |||
(invoke-multi impl mta a b c d)) | |||
|
|||
([^MultiFnImpl impl mta a b c d & more] | |||
([^MultiFnImpl impl mta a b c d e] | |||
#_(println "invoke-multifn 5-arity") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#_(println "invoke-multifn 5-arity") |
As @bshepherdson mentioned in #149, a lot of the overhead that Methodical brings comes from packing/unpacking arglists in vararg functions, and that in turn is primarily caused by
with-meta
on functions creating very inefficientRestFn
objects.When I say "overhead", I'm talking about 300 ns and ~500 bytes allocated per call of a 2-argument Methodical multimethod, vs 15ns and 0b when called an identical Clojure multimethod. The allocation part is especially damning as it creates memory and GC pressure and prevents JIT from optimizing nested
fn
s (and Methodical likes nesting lambdas a lot).I've made several attempts to address this:
invoke()
calls to the stored function and, obviously, stores metadata in a field.This PR contains my last attempt. All tests pass and everything seems to be working. The performance of 2-arity method call is back to 12-15ns and 0b (so it's even somewhat faster than Clojure multimethods). This is a screenshot of the profile:
There are two drawbacks to this solution:
add-primary-method
etc. I had to adapt a few tests for this.clojure -T:build javac
before running tests or starting the REPL.Eventually, #149 or a variation of it will have to be merged too in order to ensure wrapping/unwrapping of arglists doesn't happen in 4,5,6-argument multimethods which are quite popular in Toucan, for example.