devirtualize protocol dispatch on known record receivers (jolt-41m)
A protocol method call compiles to (protocol-dispatch proto method this rest) — a
runtime registry walk (type-tag -> proto -> method) on every call, ~19x a direct
call. When the inference proves the receiver (arg 0) is a known record type, the
call now resolves to a DIRECT method call at compile time, skipping the registry.
- defprotocol registers each method's var-key 'ns/method' -> [proto method] (a
ctx-capturing register-protocol-methods! emitted into the do-block); infer-unit!
feeds it to the inference via a box (like record-shapes).
- the record-ctor return type carries :type (the record tag) so the inference
knows the receiver type; the :else invoke case annotates a protocol call whose
arg0 has a known :type with :devirt-{type,proto,method}.
- emit-invoke resolves the impl via find-protocol-method at emit time and emits a
direct call to the embedded impl fn value. Unknown/polymorphic receivers (no
proven :type) fall back to the dispatch path unchanged.
Measured: removes the dispatch overhead (14.7s -> 9.3s on a 10M-call loop); the
remaining cost is the method body itself (non-inlined, unproven reads) — inlining
the resolved method is the follow-up (jolt-t6r) toward direct-call speed.
Sound under the closed-world assumption direct-linking already makes (the impl is
resolved + embedded at compile time). Adds devirt-test (subprocess: dispatched ==
devirtualized across polymorphic dispatch, unknown-receiver fallback, and
heterogeneous collections). Stalin's compile-call/callee-environment is the model.
This commit is contained in:
parent
a83792cc51
commit
fa02c8f93d
5 changed files with 94 additions and 11 deletions
|
|
@ -318,6 +318,9 @@
|
||||||
{} sigs)]
|
{} sigs)]
|
||||||
`(do
|
`(do
|
||||||
(def ~pname (make-protocol ~(name pname) ~methods))
|
(def ~pname (make-protocol ~(name pname) ~methods))
|
||||||
|
;; register method var-keys for devirtualization (jolt-41m); the inference
|
||||||
|
;; reads this (via infer-unit!) to resolve a protocol call on a known record
|
||||||
|
(register-protocol-methods! ~(name pname) [~@(map (fn [s] (name (first s))) sigs)])
|
||||||
~@(map (fn [sig]
|
~@(map (fn [sig]
|
||||||
`(def ~(first sig)
|
`(def ~(first sig)
|
||||||
;; protocol-dispatch is a fn (clojure.core); pass the protocol /
|
;; protocol-dispatch is a fn (clojure.core); pass the protocol /
|
||||||
|
|
|
||||||
|
|
@ -881,6 +881,9 @@
|
||||||
;; struct of this shape, so field reads on the result bare-index — declared
|
;; struct of this shape, so field reads on the result bare-index — declared
|
||||||
;; shapes are clean fuel: a lookup, not fragile inference.
|
;; shapes are clean fuel: a lookup, not fragile inference.
|
||||||
(def ^:private record-shapes-box (atom {}))
|
(def ^:private record-shapes-box (atom {}))
|
||||||
|
;; jolt-41m: protocol-method registry "ns/method" -> [proto method], for
|
||||||
|
;; devirtualizing a protocol call whose receiver is a known record type.
|
||||||
|
(def ^:private protocol-methods-box (atom {}))
|
||||||
;; jolt-t34: whether to shape generic const-key MAP literals (opt-in, JOLT_SHAPE).
|
;; jolt-t34: whether to shape generic const-key MAP literals (opt-in, JOLT_SHAPE).
|
||||||
;; Records are shaped regardless; maps only when this is on.
|
;; Records are shaped regardless; maps only when this is on.
|
||||||
(def ^:private map-shapes-box (atom false))
|
(def ^:private map-shapes-box (atom false))
|
||||||
|
|
@ -909,9 +912,11 @@
|
||||||
(= op :var) (let [rs (get @record-shapes-box (var-key fnode))]
|
(= op :var) (let [rs (get @record-shapes-box (var-key fnode))]
|
||||||
(if rs
|
(if rs
|
||||||
;; record ctor -> struct of declared shape (jolt-t34); :shape
|
;; record ctor -> struct of declared shape (jolt-t34); :shape
|
||||||
;; is the DECLARED field order, which the back end indexes by
|
;; is the DECLARED field order the back end indexes by, and
|
||||||
(assoc (mk-struct (reduce (fn [m k] (assoc m k :any)) {} rs))
|
;; :type is the record tag (for devirtualizing protocol calls)
|
||||||
:shape (vec rs))
|
(let [fields (get rs :fields)]
|
||||||
|
(assoc (mk-struct (reduce (fn [m k] (assoc m k :any)) {} fields))
|
||||||
|
:shape (vec fields) :type (get rs :type)))
|
||||||
(let [r (get @rtenv-box (var-key fnode))]
|
(let [r (get @rtenv-box (var-key fnode))]
|
||||||
(if r r (let [nm (and (= "clojure.core" (get fnode :ns)) (get fnode :name))]
|
(if r r (let [nm (and (= "clojure.core" (get fnode :ns)) (get fnode :name))]
|
||||||
(cond (nil? nm) :any
|
(cond (nil? nm) :any
|
||||||
|
|
@ -1116,13 +1121,23 @@
|
||||||
(when (and @strict-box iscall-var)
|
(when (and @strict-box iscall-var)
|
||||||
(let [k (var-key fnode) usig (get @user-sig-box k)]
|
(let [k (var-key fnode) usig (get @user-sig-box k)]
|
||||||
(when usig (check-user-call k usig ats pos))))))
|
(when usig (check-user-call k usig ats pos))))))
|
||||||
[(cond
|
;; devirtualization (jolt-41m): a protocol-method call whose receiver
|
||||||
(= cn "range") (mk-vec :num)
|
;; (arg 0) is a known record type resolves to a direct method call.
|
||||||
;; element-returning fn over a typed vector -> the element type
|
;; Annotate the node with [type-tag proto method]; the back end looks
|
||||||
(and cn (contains? elem-fns cn) (> n 0))
|
;; up the impl at emit time and calls it directly, skipping the
|
||||||
(let [a0 (nth (nth ares 0) 0)] (if (vec-type? a0) (velem a0) :any))
|
;; registry dispatch (~19x cheaper than protocol-dispatch).
|
||||||
:else (call-ret-type fnode))
|
(let [pm (and iscall-var (get @protocol-methods-box (var-key fnode)))
|
||||||
(assoc node :fn fnode' :args (mapv (fn [r] (nth r 1)) ares))])))
|
rtype (when (and pm (pos? n)) (get (nth (nth ares 0) 0) :type))
|
||||||
|
base (assoc node :fn fnode' :args (mapv (fn [r] (nth r 1)) ares))]
|
||||||
|
[(cond
|
||||||
|
(= cn "range") (mk-vec :num)
|
||||||
|
;; element-returning fn over a typed vector -> the element type
|
||||||
|
(and cn (contains? elem-fns cn) (> n 0))
|
||||||
|
(let [a0 (nth (nth ares 0) 0)] (if (vec-type? a0) (velem a0) :any))
|
||||||
|
:else (call-ret-type fnode))
|
||||||
|
(if rtype
|
||||||
|
(assoc base :devirt-type rtype :devirt-proto (nth pm 0) :devirt-method (nth pm 1))
|
||||||
|
base)]))))
|
||||||
(= op :let)
|
(= op :let)
|
||||||
(let [res (reduce (fn [acc b]
|
(let [res (reduce (fn [acc b]
|
||||||
(let [te (nth acc 0) binds (nth acc 1)
|
(let [te (nth acc 0) binds (nth acc 1)
|
||||||
|
|
@ -1330,6 +1345,7 @@
|
||||||
;; jolt-t34: install record-ctor shapes ("ns/->Name" -> [field-kw ...]) and the
|
;; jolt-t34: install record-ctor shapes ("ns/->Name" -> [field-kw ...]) and the
|
||||||
;; map-shaping flag (opt-in JOLT_SHAPE), both read by infer.
|
;; map-shaping flag (opt-in JOLT_SHAPE), both read by infer.
|
||||||
(defn set-record-shapes! [m] (reset! record-shapes-box (or m {})))
|
(defn set-record-shapes! [m] (reset! record-shapes-box (or m {})))
|
||||||
|
(defn set-protocol-methods! [m] (reset! protocol-methods-box (or m {})))
|
||||||
(defn set-map-shapes! [b] (reset! map-shapes-box (boolean b)))
|
(defn set-map-shapes! [b] (reset! map-shapes-box (boolean b)))
|
||||||
|
|
||||||
(defn set-vtypes!
|
(defn set-vtypes!
|
||||||
|
|
|
||||||
|
|
@ -417,7 +417,16 @@
|
||||||
(def args (map |(emit ctx $) (vview (node :args))))
|
(def args (map |(emit ctx $) (vview (node :args))))
|
||||||
(def nop (native-op fnode (length args)))
|
(def nop (native-op fnode (length args)))
|
||||||
(def argnodes (vview (node :args)))
|
(def argnodes (vview (node :args)))
|
||||||
|
# devirtualization (jolt-41m): the inference proved this is a protocol call on a
|
||||||
|
# known record type. Resolve the method impl at COMPILE time and emit a direct
|
||||||
|
# call to it, skipping the runtime protocol-dispatch registry walk. The impl is
|
||||||
|
# embedded as a constant fn value in the call head.
|
||||||
|
(def dvt (node :devirt-type))
|
||||||
|
(def devirt-impl
|
||||||
|
(when dvt (find-protocol-method ctx dvt (node :devirt-proto) (node :devirt-method))))
|
||||||
(cond
|
(cond
|
||||||
|
devirt-impl (tuple devirt-impl ;args)
|
||||||
|
|
||||||
nop (case nop
|
nop (case nop
|
||||||
'++ ['+ (in args 0) 1]
|
'++ ['+ (in args 0) 1]
|
||||||
'-- ['- (in args 0) 1]
|
'-- ['- (in args 0) 1]
|
||||||
|
|
@ -952,6 +961,7 @@
|
||||||
(def f-reset-esc (and pns (ns-find pns "reset-escapes!")))
|
(def f-reset-esc (and pns (ns-find pns "reset-escapes!")))
|
||||||
(def f-set-rshapes (and pns (ns-find pns "set-record-shapes!"))) # jolt-t34
|
(def f-set-rshapes (and pns (ns-find pns "set-record-shapes!"))) # jolt-t34
|
||||||
(def f-set-mshapes (and pns (ns-find pns "set-map-shapes!"))) # jolt-t34
|
(def f-set-mshapes (and pns (ns-find pns "set-map-shapes!"))) # jolt-t34
|
||||||
|
(def f-set-pmethods (and pns (ns-find pns "set-protocol-methods!"))) # jolt-41m
|
||||||
(def f-get-esc (and pns (ns-find pns "collected-escapes")))
|
(def f-get-esc (and pns (ns-find pns "collected-escapes")))
|
||||||
(def report @{})
|
(def report @{})
|
||||||
(when (and f-set-rtenv f-set-vtypes f-join f-infer-body f-reinfer f-reset-esc f-get-esc)
|
(when (and f-set-rtenv f-set-vtypes f-join f-infer-body f-reinfer f-reset-esc f-get-esc)
|
||||||
|
|
@ -989,6 +999,8 @@
|
||||||
# jolt-t34: feed record-ctor shapes + the map-shaping flag to the inference
|
# jolt-t34: feed record-ctor shapes + the map-shaping flag to the inference
|
||||||
(when f-set-rshapes ((var-get f-set-rshapes) (or (get (ctx :env) :record-shapes) @{})))
|
(when f-set-rshapes ((var-get f-set-rshapes) (or (get (ctx :env) :record-shapes) @{})))
|
||||||
(when f-set-mshapes ((var-get f-set-mshapes) (get (ctx :env) :map-shapes?)))
|
(when f-set-mshapes ((var-get f-set-mshapes) (get (ctx :env) :map-shapes?)))
|
||||||
|
# jolt-41m: feed the protocol-method registry for devirtualization
|
||||||
|
(when f-set-pmethods ((var-get f-set-pmethods) (or (get (ctx :env) :protocol-methods) @{})))
|
||||||
# --- param/return/value-type fixpoint (chaotic iteration to LEAST fixpoint) ---
|
# --- param/return/value-type fixpoint (chaotic iteration to LEAST fixpoint) ---
|
||||||
# Param types are RECOMPUTED FRESH each iteration, not accumulated: :any is
|
# Param types are RECOMPUTED FRESH each iteration, not accumulated: :any is
|
||||||
# the lattice top, so a join with an early-iteration :any (a caller whose own
|
# the lattice top, so a join with an early-iteration :any (a caller whose own
|
||||||
|
|
|
||||||
|
|
@ -1381,7 +1381,8 @@
|
||||||
# the IR names the call head. Harmless when records aren't shaped (sidx gated).
|
# the IR names the call head. Harmless when records aren't shaped (sidx gated).
|
||||||
(let [rs (or (get (ctx :env) :record-shapes)
|
(let [rs (or (get (ctx :env) :record-shapes)
|
||||||
(let [t @{}] (put (ctx :env) :record-shapes t) t))]
|
(let [t @{}] (put (ctx :env) :record-shapes t) t))]
|
||||||
(put rs (string (ctx-current-ns ctx) "/->" (type-name-sym :name)) (tuple ;kws)))
|
(put rs (string (ctx-current-ns ctx) "/->" (type-name-sym :name))
|
||||||
|
{:fields (tuple ;kws) :type type-tag}))
|
||||||
# Records are shape-recs when shapes are active (:shapes? = direct-link, where
|
# Records are shape-recs when shapes are active (:shapes? = direct-link, where
|
||||||
# the inference proves the reads) — the whole field-access pipeline handles
|
# the inference proves the reads) — the whole field-access pipeline handles
|
||||||
# them; otherwise the original :jolt/deftype tables. Read at ctor-BUILD time so
|
# them; otherwise the original :jolt/deftype tables. Read at ctor-BUILD time so
|
||||||
|
|
@ -1409,6 +1410,16 @@
|
||||||
(ns-intern core "protocol-dispatch"
|
(ns-intern core "protocol-dispatch"
|
||||||
(fn [proto-name method-name obj rest-args]
|
(fn [proto-name method-name obj rest-args]
|
||||||
(protocol-dispatch-impl ctx proto-name method-name obj rest-args)))
|
(protocol-dispatch-impl ctx proto-name method-name obj rest-args)))
|
||||||
|
# Devirtualization registry (jolt-41m): defprotocol calls this at load so the
|
||||||
|
# inference can recognize a protocol-method call site. Maps the method's
|
||||||
|
# var-key "ns/method" -> [proto-name method-name].
|
||||||
|
(ns-intern core "register-protocol-methods!"
|
||||||
|
(fn [proto-name method-names]
|
||||||
|
(def reg (or (get (ctx :env) :protocol-methods)
|
||||||
|
(let [t @{}] (put (ctx :env) :protocol-methods t) t)))
|
||||||
|
(def ns (ctx-current-ns ctx))
|
||||||
|
(each m (d-realize method-names) (put reg (string ns "/" m) (tuple proto-name m)))
|
||||||
|
nil))
|
||||||
(ns-intern core "extenders"
|
(ns-intern core "extenders"
|
||||||
(fn [proto]
|
(fn [proto]
|
||||||
# All type-tags whose registry entry implements this protocol, as symbols
|
# All type-tags whose registry entry implements this protocol, as symbols
|
||||||
|
|
|
||||||
41
test/integration/devirt-test.janet
Normal file
41
test/integration/devirt-test.janet
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
# Protocol-dispatch devirtualization (jolt-41m): when the inference proves a
|
||||||
|
# protocol call's receiver is a known record type, the call is compiled to a
|
||||||
|
# DIRECT method call, skipping the runtime dispatch registry. This must stay
|
||||||
|
# SOUND — same results as the dispatched path — including polymorphic dispatch
|
||||||
|
# (the right method per type), fallback when the receiver type is unknown, and
|
||||||
|
# heterogeneous collections. Runs a protocol+record program through the built
|
||||||
|
# binary (devirt needs infer-unit!, which runs on ns load, not eval-string) and
|
||||||
|
# checks the output. Skips cleanly if build/jolt is absent.
|
||||||
|
(def jolt "build/jolt")
|
||||||
|
|
||||||
|
(defn- run [whole?]
|
||||||
|
(def dir (string (or (os/getenv "TMPDIR") "/tmp") "/jolt-dv-test"))
|
||||||
|
(os/mkdir dir)
|
||||||
|
(spit (string dir "/dv.clj")
|
||||||
|
(string
|
||||||
|
"(ns dv)\n"
|
||||||
|
"(defprotocol Shape (area [s]) (kind [s]))\n"
|
||||||
|
"(defrecord Rect [w h] Shape (area [r] (* (:w r) (:h r))) (kind [_] :rect))\n"
|
||||||
|
"(defrecord Circ [r] Shape (area [c] (* 3 (:r c) (:r c))) (kind [_] :circ))\n"
|
||||||
|
"(defn poly [s] (area s))\n" # receiver unknown -> must fall back
|
||||||
|
"(defn -main []\n"
|
||||||
|
" (println (area (->Rect 3 4)) (area (->Circ 5))\n" # devirt: 12 75
|
||||||
|
" (kind (->Rect 1 1)) (kind (->Circ 1))\n" # devirt: :rect :circ
|
||||||
|
" (poly (->Rect 3 4)) (poly (->Circ 5))\n" # fallback: 12 75
|
||||||
|
" (mapv area [(->Rect 2 3) (->Circ 2)])))\n")) # heterogeneous: [6 12]
|
||||||
|
(def out (string dir "/out.txt"))
|
||||||
|
(def jbin (string (os/cwd) "/" jolt))
|
||||||
|
(def cmd (string (if whole? "JOLT_WHOLE_PROGRAM=1 " "")
|
||||||
|
"JOLT_DIRECT_LINK=1 JOLT_PATH=" dir " " jbin " -m dv > " out " 2>&1"))
|
||||||
|
(os/execute ["sh" "-c" cmd] :p)
|
||||||
|
(string/trimr (slurp out)))
|
||||||
|
|
||||||
|
(def expected "12 75 :rect :circ 12 75 [6 12]")
|
||||||
|
(if (not (os/stat jolt))
|
||||||
|
(print "devirt: SKIP (no build/jolt — run from source)")
|
||||||
|
(let [per-ns (run false) whole (run true)]
|
||||||
|
(printf " per-ns: %s" per-ns)
|
||||||
|
(printf " whole-program: %s" whole)
|
||||||
|
(if (and (= per-ns expected) (= whole expected))
|
||||||
|
(print "devirt: correct (dispatched == devirtualized)")
|
||||||
|
(do (printf "devirt: WRONG — expected %q" expected) (os/exit 1)))))
|
||||||
Loading…
Add table
Add a link
Reference in a new issue