shapes: revert default-on to opt-in; faster get-or-shape inline read (jolt-t34)
Measurement (vec-heavy benchmark + read-mechanism micro-benchmarks) showed that shape-rec'ing generic const-key maps is a net loss in a bytecode VM: an unproven field read can't beat a native Janet struct-get (one opcode), and an inline cache doesn't transfer to a VM jolt doesn't control. The win is real only for PROVEN reads (bare-index beats struct) — i.e. construction-heavy or inference-friendly code, not general map-through-fn code. So shapes are opt-in again (JOLT_SHAPE), not defaulted on in direct-link. Kept the get-or-shape fix: the non-proven shape read now indexes inline off the descriptor instead of calling core-get (which was ~2.5x slower per read). :shapes? is the single opt-in gate; image cache keys on JOLT_SHAPE + JOLT_NO_SHAPE. Next: records with declared shapes get fast field access by default (the general case), which is where the proven-read win actually lands.
This commit is contained in:
parent
7900173d45
commit
4e7b8f792f
3 changed files with 35 additions and 19 deletions
|
|
@ -223,6 +223,12 @@
|
||||||
# the inline pass only inlines targets that won't be redefined, the same
|
# the inline pass only inlines targets that won't be redefined, the same
|
||||||
# safety the direct-linking flag asserts (jolt-87f).
|
# safety the direct-linking flag asserts (jolt-87f).
|
||||||
(put (ctx :env) :inline? (if (get (ctx :env) :direct-linking?) true false))
|
(put (ctx :env) :inline? (if (get (ctx :env) :direct-linking?) true false))
|
||||||
|
# Shape-records are OPT-IN (jolt-t34). Measurement showed shaping generic
|
||||||
|
# const-key maps net-loses in a bytecode VM (unproven reads can't beat a
|
||||||
|
# native struct-get), so shapes are not defaulted on. The win is records
|
||||||
|
# with declared shapes + proven reads; that path is enabled separately.
|
||||||
|
(put (ctx :env) :shapes?
|
||||||
|
(and (os/getenv "JOLT_SHAPE") (not (os/getenv "JOLT_NO_SHAPE"))))
|
||||||
ctx))
|
ctx))
|
||||||
|
|
||||||
# --- Context snapshot/fork (cheap isolated copies) --------------------------
|
# --- Context snapshot/fork (cheap isolated copies) --------------------------
|
||||||
|
|
@ -300,7 +306,7 @@
|
||||||
# Opts land in the key via their printed form; an opt that prints unstably
|
# Opts land in the key via their printed form; an opt that prints unstably
|
||||||
# (e.g. a closure in :namespaces) just degrades to a cache miss, never to a
|
# (e.g. a closure in :namespaces) just degrades to a cache miss, never to a
|
||||||
# wrong hit. Runtime knobs that shape the ctx outside opts ride along too.
|
# wrong hit. Runtime knobs that shape the ctx outside opts ride along too.
|
||||||
(def key (string/format "%q|%q|%q|%q|%q|%q|%q|%q|%q|%q|%q"
|
(def key (string/format "%q|%q|%q|%q|%q|%q|%q|%q|%q|%q|%q|%q"
|
||||||
(string janet/version "-" janet/build)
|
(string janet/version "-" janet/build)
|
||||||
opts
|
opts
|
||||||
(os/getenv "JOLT_PATH")
|
(os/getenv "JOLT_PATH")
|
||||||
|
|
@ -311,8 +317,10 @@
|
||||||
(os/getenv "JOLT_DIRECT_LINK")
|
(os/getenv "JOLT_DIRECT_LINK")
|
||||||
(os/getenv "JOLT_NO_IR_PASSES")
|
(os/getenv "JOLT_NO_IR_PASSES")
|
||||||
(os/getenv "JOLT_CHECK_HINTS")
|
(os/getenv "JOLT_CHECK_HINTS")
|
||||||
# JOLT_SHAPE shapes core's own compiled get/record paths
|
# :shapes? is baked into the image; key on every input
|
||||||
(os/getenv "JOLT_SHAPE")))
|
# to it so a cache hit never carries a wrong shape state
|
||||||
|
(os/getenv "JOLT_SHAPE")
|
||||||
|
(os/getenv "JOLT_NO_SHAPE")))
|
||||||
(string dir "/jolt-ctx-" (band h 0x7FFFFFFF) "-" len "-" (band (hash key) 0x7FFFFFFF) ".jimg"))
|
(string dir "/jolt-ctx-" (band h 0x7FFFFFFF) "-" len "-" (band (hash key) 0x7FFFFFFF) ".jimg"))
|
||||||
|
|
||||||
(defn init-cached
|
(defn init-cached
|
||||||
|
|
|
||||||
|
|
@ -355,7 +355,7 @@
|
||||||
# - ^:struct / ^Record hinted subject: skip the guard, bare get (~20 vs ~36ns).
|
# - ^:struct / ^Record hinted subject: skip the guard, bare get (~20 vs ~36ns).
|
||||||
# - hinted + JOLT_CHECK_HINTS: keep the guard but THROW on the tagged arm, so a
|
# - hinted + JOLT_CHECK_HINTS: keep the guard but THROW on the tagged arm, so a
|
||||||
# lying hint surfaces a clear error (dev aid; off by default, no perf cost).
|
# lying hint surfaces a clear error (dev aid; off by default, no perf cost).
|
||||||
(defn- emit-kw-lookup [subj-node m-expr k d-expr]
|
(defn- emit-kw-lookup [ctx subj-node m-expr k d-expr]
|
||||||
# the subject is a struct (raw-get-safe) when hinted so — by an explicit
|
# the subject is a struct (raw-get-safe) when hinted so — by an explicit
|
||||||
# ^:struct/^Record hint on a local, OR by inference tagging ANY subject
|
# ^:struct/^Record hint on a local, OR by inference tagging ANY subject
|
||||||
# expression it proved to be a struct (jolt-d6u/RFC 0005), which is what lets
|
# expression it proved to be a struct (jolt-d6u/RFC 0005), which is what lets
|
||||||
|
|
@ -374,24 +374,28 @@
|
||||||
# comes from the runtime's canonical shape-sort (the same order shape-for and
|
# comes from the runtime's canonical shape-sort (the same order shape-for and
|
||||||
# emit-map use), so construction and lookup always agree.
|
# emit-map use), so construction and lookup always agree.
|
||||||
(def sidx
|
(def sidx
|
||||||
(when (and (os/getenv "JOLT_SHAPE") subj-node (subj-node :shape))
|
(when (and (get (ctx :env) :shapes?) subj-node (subj-node :shape))
|
||||||
(def raw (let [s (subj-node :shape)] (if (pv/pvec? s) (pv/pv->array s) s)))
|
(def raw (let [s (subj-node :shape)] (if (pv/pvec? s) (pv/pv->array s) s)))
|
||||||
(def sk (shape-sort raw))
|
(def sk (shape-sort raw))
|
||||||
(var pos nil) (var i 0)
|
(var pos nil) (var i 0)
|
||||||
(each kk sk (when (= kk k) (set pos i)) (++ i))
|
(each kk sk (when (= kk k) (set pos i)) (++ i))
|
||||||
(when pos (+ pos 1))))
|
(when pos (+ pos 1))))
|
||||||
# sidx: complete shape proven -> bare index (fastest). Otherwise, with shapes
|
# sidx => proven complete shape: bare index (fastest). Otherwise a raw-get-safe
|
||||||
# on, a raw-get-safe value may still be a shape-rec (its :shape dropped by a
|
# value may still be a shape-rec (its :shape dropped by a join, or an unproven
|
||||||
# join), so read the index from the value's own descriptor when it is a tuple;
|
# param). Read it INLINE from its own descriptor — a shape-rec is a tuple whose
|
||||||
# a real struct takes the bare get. (jolt-t34)
|
# head is a descriptor struct; index the present field directly. A missing key
|
||||||
# sidx => proven complete shape, bare index (the fast path). Otherwise route a
|
# (incl. the virtual :jolt/deftype) falls to core-get, which is nil-safe and
|
||||||
# TUPLE (a possible shape-rec) to core-get, which is shape-aware and nil-safe;
|
# shape-aware; a struct/other takes the bare get. The inline read matters: a
|
||||||
# a struct takes the bare get. NOT gated on compile-time JOLT_SHAPE: core is
|
# core-get fn call per field read is ~2.5x slower on map-through-fn code.
|
||||||
# baked WITHOUT the flag but still receives user shape-recs, so this must hold
|
# NOT gated on compile-time shapes — core is baked without the flag but still
|
||||||
# in baked core too. Cost on the default path is one tuple? test per
|
# receives user shape-recs, so this must hold in baked core too. (jolt-t34)
|
||||||
# non-proven keyword lookup. (jolt-t34 R3)
|
|
||||||
(defn get-or-shape [getexpr]
|
(defn get-or-shape [getexpr]
|
||||||
(if sidx ['in m sidx] ['if ['tuple? m] (tuple core-get m k nil) getexpr]))
|
(if sidx ['in m sidx]
|
||||||
|
(let [pos (jsym)]
|
||||||
|
['if ['and ['tuple? m] ['struct? ['get m 0]]]
|
||||||
|
['let [pos ['get [['get m 0] :idx] k]]
|
||||||
|
['if ['nil? pos] (tuple core-get m k nil) ['in m ['+ 1 pos]]]]
|
||||||
|
getexpr])))
|
||||||
(if (nil? d-expr)
|
(if (nil? d-expr)
|
||||||
(let [fast (get-or-shape ['get m k])]
|
(let [fast (get-or-shape ['get m k])]
|
||||||
(wrap (cond
|
(wrap (cond
|
||||||
|
|
@ -435,7 +439,7 @@
|
||||||
# the subject skips the guard entirely (jolt-94n; see emit-kw-lookup).
|
# the subject skips the guard entirely (jolt-94n; see emit-kw-lookup).
|
||||||
(and (= :const (fnode :op)) (keyword? (fnode :val))
|
(and (= :const (fnode :op)) (keyword? (fnode :val))
|
||||||
(>= 2 (length args) 1))
|
(>= 2 (length args) 1))
|
||||||
(emit-kw-lookup (norm-node (in argnodes 0)) (in args 0) (fnode :val)
|
(emit-kw-lookup ctx (norm-node (in argnodes 0)) (in args 0) (fnode :val)
|
||||||
(when (= 2 (length args)) (in args 1)))
|
(when (= 2 (length args)) (in args 1)))
|
||||||
# (get m :kw) / (get m :kw default) — same inlined keyword lookup as (:kw m),
|
# (get m :kw) / (get m :kw default) — same inlined keyword lookup as (:kw m),
|
||||||
# so an explicit get with a constant keyword gets the guard fast path and the
|
# so an explicit get with a constant keyword gets the guard fast path and the
|
||||||
|
|
@ -443,7 +447,7 @@
|
||||||
# a variable/number/string key falls through to core-get below.
|
# a variable/number/string key falls through to core-get below.
|
||||||
(and (get-head? fnode) (>= (length args) 2) (<= (length args) 3)
|
(and (get-head? fnode) (>= (length args) 2) (<= (length args) 3)
|
||||||
(let [a1 (norm-node (in argnodes 1))] (and (= :const (a1 :op)) (keyword? (a1 :val)))))
|
(let [a1 (norm-node (in argnodes 1))] (and (= :const (a1 :op)) (keyword? (a1 :val)))))
|
||||||
(emit-kw-lookup (norm-node (in argnodes 0)) (in args 0)
|
(emit-kw-lookup ctx (norm-node (in argnodes 0)) (in args 0)
|
||||||
((norm-node (in argnodes 1)) :val)
|
((norm-node (in argnodes 1)) :val)
|
||||||
(when (= 3 (length args)) (in args 2)))
|
(when (= 3 (length args)) (in args 2)))
|
||||||
# (count v) on an inferred vector -> pv-count, skipping core-count's dispatch
|
# (count v) on an inferred vector -> pv-count, skipping core-count's dispatch
|
||||||
|
|
@ -503,7 +507,7 @@
|
||||||
# compiler's own IR-node map literals (which the back end reads with raw
|
# compiler's own IR-node map literals (which the back end reads with raw
|
||||||
# keyword access and would break if turned into tuples)
|
# keyword access and would break if turned into tuples)
|
||||||
(def shape-keys
|
(def shape-keys
|
||||||
(when (and fast (os/getenv "JOLT_SHAPE") (get (ctx :env) :inline?))
|
(when (and fast (get (ctx :env) :shapes?) (get (ctx :env) :inline?))
|
||||||
(def ks @[])
|
(def ks @[])
|
||||||
(each pair pairs (array/push ks ((norm-node (in (vview pair) 0)) :val)))
|
(each pair pairs (array/push ks ((norm-node (in (vview pair) 0)) :val)))
|
||||||
(shape-sort ks)))
|
(shape-sort ks)))
|
||||||
|
|
|
||||||
|
|
@ -506,6 +506,10 @@
|
||||||
(when (= "1" (os/getenv "JOLT_DIRECT_LINK"))
|
(when (= "1" (os/getenv "JOLT_DIRECT_LINK"))
|
||||||
(put (ctx :env) :direct-linking? true)
|
(put (ctx :env) :direct-linking? true)
|
||||||
(put (ctx :env) :inline? true))
|
(put (ctx :env) :inline? true))
|
||||||
|
# Recompute :shapes? from the runtime env (the baked ctx computed it at build
|
||||||
|
# time, before JOLT_SHAPE was set in this process). Opt-in (jolt-t34).
|
||||||
|
(put (ctx :env) :shapes?
|
||||||
|
(and (os/getenv "JOLT_SHAPE") (not (os/getenv "JOLT_NO_SHAPE"))))
|
||||||
(cond
|
(cond
|
||||||
(empty? argv) (run-repl)
|
(empty? argv) (run-repl)
|
||||||
(help-flags (argv 0)) (print-help)
|
(help-flags (argv 0)) (print-help)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue