perf: hint-driven keyword-lookup guard elimination (^:struct)
A constant-keyword lookup (:k m) currently emits a guarded form, (if (get m :jolt/type) (core-get m k) (get m k)), to tell a plain struct (raw get is correct) from a phm/sorted/transient (needs core-get). On a struct that guard is a second get, so the lookup costs ~36ns where a bare get is ~20ns. Profiling the ray tracer (jolt-dad) showed keyword lookups are ~50% of a render and the guard is the only avoidable part, but dropping it needs to know statically that the subject is a plain struct. Type hints are exactly that information, and jolt already parses them and otherwise ignores them. This wires one through: a local hinted ^:struct asserts a plain struct/record map, so a (:k local) lookup on it skips the guard and emits a bare get. The hint rides on the binding symbol into the analyzer, which records it per-local and attaches it to :local IR nodes; the back end reads it on the lookup subject. It also propagates through inlining: when the inliner let-binds a non-trivial arg to a fresh local, it carries the called fn's param hint onto that local, so lookups inside the spliced body keep the bare path. This is a programmer assertion, like a Clojure type hint (an inaccurate hint just makes the raw get return the wrong value, the same contract as a wrong ^String), so it stays opt-in and off by default. On the ray tracer (with inlining on) this is 13.3s to 10.9s, 1.22x, taking it to 7.8x JVM from 9.4x after the inline pass. The unhinted path emits identical code (the fast arm is just factored out), so nothing changes without hints. Verified: a seeded full render produces an identical checksum hinted vs unhinted; conformance 335/335 in all three modes and the full jpm test pass; new test/integration/struct-hint-test.janet pins the guard removal, the inline propagation, and that an accurate hint is correctness-preserving.
This commit is contained in:
parent
e41832c05d
commit
c4be5d8a0e
4 changed files with 89 additions and 13 deletions
|
|
@ -39,11 +39,21 @@
|
|||
(swap! gensym-counter inc)
|
||||
(str "_r$" prefix n)))
|
||||
|
||||
(defn- empty-env [] {:locals #{}})
|
||||
(defn- empty-env [] {:locals #{} :hints {}})
|
||||
(defn- local? [env nm] (contains? (:locals env) nm))
|
||||
(defn- add-locals [env names] (update env :locals #(reduce conj % names)))
|
||||
(defn- with-recur [env name] (assoc env :recur name))
|
||||
|
||||
;; Type hints (jolt-dad). The reader keeps ^hint metadata on the binding symbol;
|
||||
;; we recognize ^:struct, which asserts the value is a plain struct/record map so
|
||||
;; a constant-keyword lookup can skip the :jolt/type guard and emit a bare get.
|
||||
;; Other hints parse and are ignored, as before.
|
||||
(defn- hint-of [sym]
|
||||
(let [m (form-sym-meta sym)]
|
||||
(when (and m (get m :struct)) :struct)))
|
||||
(defn- add-hint [env nm h]
|
||||
(if h (assoc env :hints (assoc (:hints env) nm h)) env))
|
||||
|
||||
(defn- analyze-seq [ctx forms env]
|
||||
(let [v (mapv #(analyze ctx % env) forms)
|
||||
n (count v)]
|
||||
|
|
@ -59,20 +69,25 @@
|
|||
(when-not (form-sym? bsym) (uncompilable "destructuring binding"))
|
||||
(let [nm (form-sym-name bsym)
|
||||
init (analyze ctx (nth bvec (inc i)) env)]
|
||||
(recur (+ i 2) (add-locals env [nm]) (conj pairs [nm init]))))
|
||||
(recur (+ i 2) (add-hint (add-locals env [nm]) nm (hint-of bsym))
|
||||
(conj pairs [nm init]))))
|
||||
[pairs env])))
|
||||
|
||||
(defn- parse-params [pvec]
|
||||
(loop [i 0 fixed [] rest-name nil]
|
||||
;; :hints is a vector of [name hint] pairs (vector, not a map, so the caller
|
||||
;; folds it with a plain reduce — no reduce-over-map in the kernel subset).
|
||||
(loop [i 0 fixed [] rest-name nil hints []]
|
||||
(if (< i (count pvec))
|
||||
(let [p (nth pvec i)]
|
||||
(when-not (form-sym? p) (uncompilable "destructuring fn param"))
|
||||
(if (= "&" (form-sym-name p))
|
||||
(let [r (nth pvec (inc i))]
|
||||
(when-not (form-sym? r) (uncompilable "destructuring fn rest"))
|
||||
(recur (+ i 2) fixed (form-sym-name r)))
|
||||
(recur (inc i) (conj fixed (form-sym-name p)) rest-name)))
|
||||
{:fixed fixed :rest rest-name})))
|
||||
(recur (+ i 2) fixed (form-sym-name r) hints))
|
||||
(let [nm (form-sym-name p) h (hint-of p)]
|
||||
(recur (inc i) (conj fixed nm) rest-name
|
||||
(if h (conj hints [nm h]) hints)))))
|
||||
{:fixed fixed :rest rest-name :hints hints})))
|
||||
|
||||
(defn- analyze-arity [ctx pvec body env fn-name]
|
||||
(let [pp (parse-params (vec (form-vec-items pvec)))
|
||||
|
|
@ -88,7 +103,8 @@
|
|||
;; keeps recur targets unique per compilation unit.
|
||||
rname (gen-name (str (compile-ns ctx) "/" (or fn-name "fn") "--"))
|
||||
names (cond-> (vec fixed) rst (conj rst) fn-name (conj fn-name))
|
||||
env* (-> (add-locals env names) (with-recur rname))
|
||||
env0 (-> (add-locals env names) (with-recur rname))
|
||||
env* (reduce (fn [e pr] (add-hint e (nth pr 0) (nth pr 1))) env0 (:hints pp))
|
||||
arity {:params fixed :recur-name rname
|
||||
:body (analyze-seq ctx body env*)}]
|
||||
;; :rest only when variadic — an absent :rest reads back nil, same as before,
|
||||
|
|
@ -190,7 +206,8 @@
|
|||
(defn- analyze-symbol [ctx form env]
|
||||
(let [nm (form-sym-name form) ns (form-sym-ns form)]
|
||||
(cond
|
||||
(and (nil? ns) (local? env nm)) (local nm)
|
||||
(and (nil? ns) (local? env nm))
|
||||
(let [h (get (:hints env) nm)] (if h (assoc (local nm) :hint h) (local nm)))
|
||||
ns (let [r (resolve-global ctx form)]
|
||||
(if (= :var (:kind r))
|
||||
(var-ref (:ns r) (:name r))
|
||||
|
|
|
|||
|
|
@ -178,7 +178,15 @@
|
|||
(let [op (get node :op)]
|
||||
(cond
|
||||
(= op :local) (let [r (get env (get node :name))]
|
||||
(if r r node))
|
||||
;; carry the param's ^:struct hint onto a let-bound fresh
|
||||
;; local, so lookups inside the inlined body keep the bare
|
||||
;; (no-guard) path (jolt-dad). The param hint asserts the
|
||||
;; arg is a struct; inlining doesn't change that contract.
|
||||
(if r
|
||||
(if (and (= :local (get r :op)) (get node :hint) (not (get r :hint)))
|
||||
(assoc r :hint (get node :hint))
|
||||
r)
|
||||
node))
|
||||
(= op :if) (assoc node
|
||||
:test (subst (get node :test) env)
|
||||
:then (subst (get node :then) env)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue