automatically factor anonymous functions with pred, 20% more effient cache strategy, more efficient hot path, added ability with must-cache-paths to error when a path can't be cached and get detailed information why

This commit is contained in:
Nathan Marz 2016-05-21 09:44:20 -04:00
parent d3a462aa06
commit c2fa922717
3 changed files with 145 additions and 86 deletions

View file

@ -26,6 +26,8 @@
(defn comp-paths [& paths]
(i/comp-paths* (vec paths)))
(def must-cache-paths! i/must-cache-paths!)
;; Selection functions
(def ^{:doc "Version of select that takes in a path pre-compiled with comp-paths"}
@ -403,15 +405,12 @@
(transform* [aset structure next-fn]
(i/filter-transform aset structure next-fn)))
(defpath
(def
^{:doc "Keeps the element only if it matches the supplied predicate. This is the
late-bound parameterized version of using a function directly in a path."}
pred
[afn]
(select* [this structure next-fn]
(i/filter-select afn structure next-fn))
(transform* [this structure next-fn]
(i/filter-transform afn structure next-fn)))
i/pred*
)
(defpath
^{:doc "Navigates to the provided val if the structure is nil. Otherwise it stays

View file

@ -633,26 +633,42 @@
params-maker ; can be null
])
(def CACHE
#+clj (java.util.concurrent.ConcurrentHashMap.)
(defonce PATH-CACHE
#+clj (mutable-cell (java.util.HashMap.))
#+cljs (atom {})
)
#+clj
(defn add-cache! [k v]
(.put ^java.util.concurrent.ConcurrentHashMap CACHE k v))
(def ^:dynamic *must-cache-paths* false)
(defn must-cache-paths!
([] (must-cache-paths! true))
([v] (alter-var-root #'*must-cache-paths* (constantly v))))
#+clj
(defn get-cache [k]
(.get ^java.util.concurrent.ConcurrentHashMap CACHE k))
(defn add-path-cache! [k v]
;; This looks very inefficient but is actually the best approach
;; without real inline caches. The total number of copies will be the
;; number of Specter callsites (plus perhaps a few more if
;; multiple callsites attempt to be cached at exactly the same time).
;; Eventually it will stabilize and there won't be any more garbage generated.
;; The `select` performance using this cache strategy is ~20% faster for
;; the [:a :b :c] path use case than ConcurrentHashMap.
(let [newmap (java.util.HashMap. ^java.util.HashMap (get-cell PATH-CACHE))]
(.put newmap k v)
(set-cell! PATH-CACHE newmap)
))
#+clj
(defn get-path-cache [k]
(.get ^java.util.HashMap (get-cell PATH-CACHE) k))
#+cljs
(defn add-cache! [k v]
(swap! CACHE (fn [m] (assoc m k v))))
(swap! PATH-CACHE (fn [m] (assoc m k v))))
#+cljs
(defn get-cache [k]
(get @CACHE k))
(get @PATH-CACHE k))
(defn- extract-original-code [p]
(cond
@ -672,70 +688,109 @@
(reset! failed-atom true)
nil)
(defn- magic-precompilation* [p params-atom failed-atom]
(cond
(vector? p)
(mapv
#(magic-precompilation* % params-atom failed-atom)
p)
(instance? LocalSym p)
(magic-fail! failed-atom)
(instance? VarUse p)
(let [v (:var p)
vv (var-get v)]
(if (and (-> v meta :dynamic not)
(valid-navigator? vv))
vv
(magic-fail! failed-atom)
))
(instance? SpecialFormUse p)
(magic-fail! failed-atom)
(instance? FnInvocation p)
(let [op (:op p)
ps (:params p)]
(if (instance? VarUse op)
(let [v (:var op)
vv (var-get v)]
(if (-> v meta :dynamic)
(magic-fail! failed-atom)
(cond
(instance? ParamsNeededPath vv)
;;TODO: if all params are constants, then just bind the path right here
;;otherwise, add the params
(do
(swap! params-atom concat ps)
vv
)
(and (fn? vv) (-> vv meta :pathedfn))
(let [subpath (mapv #(magic-precompilation* % params-atom failed-atom)
ps)]
(if @failed-atom
nil
(apply vv subpath)
))
:else
(magic-fail! failed-atom)
)))
(magic-fail! failed-atom)
))
:else
(magic-fail! failed-atom)
(def pred*
(->ParamsNeededPath
(->TransformFunctions
RichPathExecutor
(fn [params params-idx vals structure next-fn]
(let [afn (aget ^objects params params-idx)]
(if (afn structure)
(next-fn params (inc params-idx) vals structure)
)))
(fn [params params-idx vals structure next-fn]
(let [afn (aget ^objects params params-idx)]
(if (afn structure)
(next-fn params (inc params-idx) vals structure)
structure
))))
1
))
(defn- magic-precompilation* [p params-atom failed-atom]
(let [magic-fail! (fn [& reason]
(if *must-cache-paths*
(println "Failed to cache path:" (apply str reason)))
(reset! failed-atom true)
nil)]
(cond
(vector? p)
(mapv
#(magic-precompilation* % params-atom failed-atom)
p)
(instance? LocalSym p)
(magic-fail! "Local symbol " (:sym p) " where navigator expected")
(instance? VarUse p)
(let [v (:var p)
vv (var-get v)]
(cond (-> v meta :dynamic)
(magic-fail! "Var " (:sym p) " is dynamic")
(valid-navigator? vv) vv
:else (magic-fail! "Var " (:sym p) " is not a navigator")
))
(instance? SpecialFormUse p)
(if (-> p :code first (= 'fn*))
(do
(swap! params-atom conj (:code p))
pred*
)
(magic-fail! "Special form " (:code p) " where navigator expected")
)
(instance? FnInvocation p)
(let [op (:op p)
ps (:params p)]
(if (instance? VarUse op)
(let [v (:var op)
vv (var-get v)]
(if (-> v meta :dynamic)
(magic-fail! "Var " (:sym op) " is dynamic")
(cond
(instance? ParamsNeededPath vv)
;;TODO: if all params are constants, then just bind the path right here
;;otherwise, add the params
;; - could extend this to see if it contains nested function calls which
;; are only on constants
(do
(swap! params-atom concat ps)
vv
)
(and (fn? vv) (-> vv meta :pathedfn))
(let [subpath (mapv #(magic-precompilation* % params-atom failed-atom)
ps)]
(if @failed-atom
nil
(apply vv subpath)
))
:else
(magic-fail! "Var " (:sym op) " must be either a parameterized "
"navigator or a higher order pathed constructor function")
)))
(magic-fail! "Code at " (extract-original-code p) " is in "
"function invocation position and must be either a parameterized "
"navigator or a higher order pathed constructor function"
)
))
:else
(if (valid-navigator? p)
p
(magic-fail! "Constant " p " is not a valid navigator"))
)))
(defn magic-precompilation [prepared-path used-locals]
(let [params-atom (atom [])
failed-atom (atom false)
path (magic-precompilation* prepared-path params-atom failed-atom)
]
(if @failed-atom
(->CachedPathInfo nil nil)
(if *must-cache-paths*
(throw-illegal "Failed to cache path")
(->CachedPathInfo nil nil))
(let [precompiled (comp-paths* path)
params-code (mapv extract-original-code @params-atom)
array-sym (gensym "array")

View file

@ -315,8 +315,8 @@
`(i/extend-protocolpath* ~protpath ~(protpath-sym protpath) ~(vec extensions)))
(defmacro defpathedfn [name & args]
(let [[n args] (m/name-with-attributes name args)]
`(def ~n (vary-meta (fn ~@args) assoc :pathedfn true))))
(let [[name args] (m/name-with-attributes name args)]
`(def ~name (vary-meta (fn ~@args) assoc :pathedfn true))))
(defn ic-prepare-path [locals-set path]
@ -344,16 +344,23 @@
path
))
;; still possible to mess this up with alter-var-root!
;; still possible to mess this up with alter-var-root
(defmacro ic! [& path] ; "inline cache"
(let [local-syms (-> &env keys set)
used-locals (vec (i/walk-select local-syms vector path))
prepared-path (ic-prepare-path local-syms (walk/macroexpand-all (vec path)))
;; TODO: will turning this into a keyword make it faster?
;; TODO: unclear if using long here versus string makes
;; a significant difference
;; - but using random longs creates possibility of collisions
;; (birthday problem)
;; - ideally could have a real inline cache that wouldn't
;; have to do any hashing/equality checking at all
;; - with invokedynamic here, could go directly to the code
;; to invoke and/or parameterize the precompiled path without
;; a bunch of checks beforehand
cache-id (str (java.util.UUID/randomUUID))
]
`(let [info# (i/get-cache ~cache-id)
`(let [info# (i/get-path-cache ~cache-id)
^com.rpl.specter.impl.CachedPathInfo info#
(if info#
@ -362,19 +369,17 @@
~prepared-path
~(mapv (fn [e] `(quote ~e)) used-locals)
)]
(i/add-cache! ~cache-id info#)
(i/add-path-cache! ~cache-id info#)
info#
))
precompiled# (.-precompiled info#)
params-maker# (.-params-maker info#)]
(cond (nil? precompiled#)
~path
(and precompiled# (nil? params-maker#))
precompiled#
:else
(i/bind-params* precompiled# (params-maker# ~@used-locals) 0)
(if (some? precompiled#)
(if (nil? params-maker#)
precompiled#
(i/bind-params* precompiled# (params-maker# ~@used-locals) 0)
)
(i/comp-paths* ~(vec path))
))
))