prototype: shape-record representation for vec3 maps (jolt-t34, JOLT_SHAPE)
Validated prototype of the hidden-class object-model change. A vec3-shaped
{:r :g :b} map literal is represented as a cheap Janet tuple [shape vb vg vr]
instead of a struct (~2x cheaper to construct); a lookup on a value the
inference PROVES is the shape reads by bare index with no runtime check.
Result on the ray tracer (direct-link): 12.3s -> 10.7s (~13% faster), with
byte-identical pixel output. The shape value flows transparently through
hit-info/ray/material containers and the colors vector; core-get handles it
(inline check, no fn call) so an unspecialized access is still correct.
Key lessons baked in: the lookup MUST compile to a bare index (a runtime
shape check, even inlined, taxed every field read and made it 2.5-3.4x
SLOWER) — so the inference gained a :shape hint (struct type with keys
exactly {:r :g :b}) that the back end turns into (in m idx). The descriptor
is quoted when embedded (its keys are a parens tuple Janet would otherwise
try to CALL).
All behind JOLT_SHAPE (off by default). Gate green, suite 4718, default-path
bench even. Scoped to the one shape; NOT yet sound in general (assumes every
vec3-shaped value is a shape-rec, true under the flag for the ray tracer) nor
fully transparent (only core-get + the inlined lookup; jolt-call/equality/
print/keys not yet covered). Those are the next steps toward a real feature.
This commit is contained in:
parent
50d8b13ca3
commit
d33fb85041
3 changed files with 93 additions and 9 deletions
|
|
@ -806,6 +806,17 @@
|
|||
;; k, if known, else :any.
|
||||
(defn- struct-safe? [t] (struct-type? t))
|
||||
(defn- field-type [t k] (if (struct-type? t) (get (sfields t) k :any) :any))
|
||||
;; vec3 shape detection (jolt-t34, prototype): a struct type whose key set is
|
||||
;; exactly {:r :g :b}. The back end represents such maps as shape tuples, so a
|
||||
;; lookup on a value PROVEN to be this shape can read by bare index with no
|
||||
;; runtime check. Scoped to the one shape for the prototype.
|
||||
(defn- vec3-shape? [t]
|
||||
(and (struct-type? t)
|
||||
(let [fs (sfields t)]
|
||||
(and (get fs :r) (get fs :g) (get fs :b) (= 3 (count (keys fs)))))))
|
||||
;; the structural hint for a subject: :shape when provably the vec3 shape (bare
|
||||
;; indexed read), else :struct when raw-get-safe.
|
||||
(defn- struct-hint [t] (if (vec3-shape? t) :shape :struct))
|
||||
;; tag a node (any expression, not just a :local) so the back end can specialize
|
||||
;; a lookup whose SUBJECT is that node — this is what makes nested access work:
|
||||
;; (:direction ray) is tagged struct, so (:r (:direction ray)) drops its guard.
|
||||
|
|
@ -929,7 +940,7 @@
|
|||
(let [t (get tenv (get node :name))]
|
||||
[(if t t :any)
|
||||
(cond
|
||||
(struct-safe? t) (assoc node :hint :struct)
|
||||
(struct-safe? t) (assoc node :hint (struct-hint t))
|
||||
(vec-type? t) (assoc node :hint :vector)
|
||||
:else node)])
|
||||
(= op :map)
|
||||
|
|
@ -987,7 +998,7 @@
|
|||
(and (= :const (get fnode :op)) (keyword? (get fnode :val)) (>= n 1) (<= n 2))
|
||||
(let [mr (infer (nth args 0) tenv)
|
||||
mt (nth mr 0)
|
||||
msub (if (struct-safe? mt) (mark-hint (nth mr 1) :struct) (nth mr 1))
|
||||
msub (if (struct-safe? mt) (mark-hint (nth mr 1) (struct-hint mt)) (nth mr 1))
|
||||
ft (field-type mt (get fnode :val))
|
||||
dr (when (= n 2) (infer (nth args 1) tenv))]
|
||||
[(if dr (join ft (nth dr 0)) ft)
|
||||
|
|
@ -998,7 +1009,7 @@
|
|||
(>= n 2) (= :const (get (nth args 1) :op)) (keyword? (get (nth args 1) :val)))
|
||||
(let [mr (infer (nth args 0) tenv)
|
||||
mt (nth mr 0)
|
||||
msub (if (struct-safe? mt) (mark-hint (nth mr 1) :struct) (nth mr 1))
|
||||
msub (if (struct-safe? mt) (mark-hint (nth mr 1) (struct-hint mt)) (nth mr 1))
|
||||
kr (infer (nth args 1) tenv)
|
||||
ft (field-type mt (get (nth args 1) :val))
|
||||
dr (when (= n 3) (infer (nth args 2) tenv))]
|
||||
|
|
|
|||
|
|
@ -355,13 +355,15 @@
|
|||
# - ^: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
|
||||
# lying hint surfaces a clear error (dev aid; off by default, no perf cost).
|
||||
# vec3 shape layout: descriptor at 0, then sorted keys [:b :g :r] at 1,2,3 (jolt-t34)
|
||||
(def- vec3-shape-idx {:b 1 :g 2 :r 3})
|
||||
(defn- emit-kw-lookup [subj-node m-expr k d-expr]
|
||||
# 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
|
||||
# expression it proved to be a struct (jolt-d6u/RFC 0005), which is what lets
|
||||
# nested access like (:r (:direction ray)) drop its guard.
|
||||
(def hinted (and subj-node (= :struct (subj-node :hint))))
|
||||
(def checked (and hinted (os/getenv "JOLT_CHECK_HINTS")))
|
||||
(def hinted (and subj-node (or (= :struct (subj-node :hint)) (= :shape (subj-node :hint)))))
|
||||
(def checked (and (= :struct (subj-node :hint)) (os/getenv "JOLT_CHECK_HINTS")))
|
||||
(def m (if (symbol? m-expr) m-expr (jsym)))
|
||||
(def wrap (fn [body] (if (symbol? m-expr) body ['let [m m-expr] body])))
|
||||
(def err (when checked
|
||||
|
|
@ -369,15 +371,24 @@
|
|||
k " " (subj-node :name) ") — value carries :jolt/type "
|
||||
"(a phm/sorted/transient/lazy-seq), not the plain "
|
||||
"struct/record the ^:struct/^Record hint asserts")]))
|
||||
(def sidx (and (os/getenv "JOLT_SHAPE") (get vec3-shape-idx k)))
|
||||
# subject PROVEN to be the vec3 shape (jolt-t34): read by bare index, no check.
|
||||
(def shaped (and sidx (= :shape (subj-node :hint))))
|
||||
# otherwise, when JOLT_SHAPE is on and the key is a vec3 field but the shape
|
||||
# isn't proven, runtime-check (tuple? + struct? of elem 0, inline opcodes).
|
||||
(defn get-or-shape [getexpr]
|
||||
(cond shaped ['in m sidx]
|
||||
sidx ['if ['and ['tuple? m] ['struct? ['in m 0]]] ['in m sidx] getexpr]
|
||||
getexpr))
|
||||
(if (nil? d-expr)
|
||||
(let [fast ['get m k]]
|
||||
(let [fast (get-or-shape ['get m k])]
|
||||
(wrap (cond
|
||||
checked ['if ['get m :jolt/type] err fast]
|
||||
hinted fast
|
||||
['if ['get m :jolt/type] (tuple core-get m k) fast])))
|
||||
(let [d (if (symbol? d-expr) d-expr (jsym))
|
||||
v (jsym)
|
||||
fast ['let [v ['get m k]] ['if ['nil? v] d v]]
|
||||
fast ['let [v (get-or-shape ['get m k])] ['if ['nil? v] d v]]
|
||||
body (cond
|
||||
checked ['if ['get m :jolt/type] err fast]
|
||||
hinted fast
|
||||
|
|
@ -470,6 +481,29 @@
|
|||
(unless (and (= :const (k :op))
|
||||
(or (keyword? kv) (string? kv) (number? kv) (boolean? kv)))
|
||||
(set fast false)))
|
||||
# Shape-record fast path (jolt-t34, JOLT_SHAPE): a vec3-shaped {:r :g :b} map
|
||||
# literal becomes a shape tuple (≈2x cheaper than a struct). Scoped to this
|
||||
# one shape for the prototype so other maps (hit-info/ray/material) stay
|
||||
# structs and assoc on them is unaffected. Values are emitted in the shape's
|
||||
# canonical (sorted-key) order; element 0 is the compile-time shape descriptor.
|
||||
(def shape-keys
|
||||
(when (and fast (os/getenv "JOLT_SHAPE"))
|
||||
(def ks @[])
|
||||
(each pair pairs (array/push ks ((norm-node (in (vview pair) 0)) :val)))
|
||||
(def srt (sorted ks))
|
||||
(when (deep= srt @[:b :g :r]) srt)))
|
||||
(if shape-keys
|
||||
(do
|
||||
(def desc (shape-for shape-keys))
|
||||
(def by-key @{})
|
||||
(each pair pairs
|
||||
(def p (vview pair))
|
||||
(put by-key ((norm-node (in p 0)) :val) (emit ctx (in p 1))))
|
||||
# quote the descriptor: it is a struct constant, and its keys are a parens
|
||||
# tuple — unquoted in code position Janet would try to CALL it ((:b :g))
|
||||
(def out @['tuple ['quote desc]])
|
||||
(each k shape-keys (array/push out (get by-key k)))
|
||||
(tuple/slice out))
|
||||
(if fast
|
||||
(do
|
||||
(def binds @[])
|
||||
|
|
@ -499,7 +533,7 @@
|
|||
(def p (vview pair))
|
||||
(array/push args (emit ctx (in p 0)))
|
||||
(array/push args (emit ctx (in p 1))))
|
||||
(tuple/slice args))))
|
||||
(tuple/slice args)))))
|
||||
|
||||
# A set literal: build (make-phs e1 e2 …) so each element is evaluated at runtime
|
||||
# then the persistent set is constructed — mirrors compiler.janet's emit-set-expr.
|
||||
|
|
|
|||
|
|
@ -560,9 +560,48 @@
|
|||
(do (var result @{}) (each k (keys m) (var in-ks false) (each k2 ks (if (deep= k k2) (do (set in-ks true) (break)))) (if (not in-ks) (put result k (m k))))
|
||||
(if (struct? m) (table/to-struct result) result))))
|
||||
|
||||
# --- shape records (hidden classes, jolt-t34) -------------------------------
|
||||
# A "shape record" is a cheap fixed-layout representation for a map literal
|
||||
# whose keys are a known compile-time set (e.g. a vec3 {:r :g :b}). It is a
|
||||
# Janet tuple [SHAPE v0 v1 ...] where SHAPE is an interned descriptor struct
|
||||
# {:jolt/shape KEYS :idx {k->pos}}. Construction is a tuple (≈2x cheaper than a
|
||||
# struct), const-keyword lookup compiles to an index, and general map ops below
|
||||
# recognize it via shape-rec? and treat it as a map — so it is transparent
|
||||
# wherever it flows. Created only by the backend when JOLT_SHAPE is on and the
|
||||
# inference proves the shape; the runtime support here is always present so a
|
||||
# shape value is handled correctly regardless.
|
||||
(def- shape-cache @{}) # sorted-keys-tuple -> interned shape descriptor
|
||||
(defn shape-for
|
||||
"Interned shape descriptor for an ordered key vector (keys in layout order)."
|
||||
[keyv]
|
||||
(def kk (tuple ;keyv))
|
||||
(or (get shape-cache kk)
|
||||
(let [idx @{}]
|
||||
(var i 0) (each k keyv (put idx k i) (++ i))
|
||||
(def desc (struct :jolt/shape kk :idx (table/to-struct idx)))
|
||||
(put shape-cache kk desc)
|
||||
desc)))
|
||||
(defn shape-rec? [x]
|
||||
(and (tuple? x) (> (length x) 0)
|
||||
(struct? (in x 0)) (not (nil? (in (in x 0) :jolt/shape)))))
|
||||
(defn shape-keys [rec] ((in rec 0) :jolt/shape))
|
||||
(defn shape-get [rec k default]
|
||||
(def pos (get ((in rec 0) :idx) k))
|
||||
(if (nil? pos) default (in rec (+ pos 1))))
|
||||
(defn shape-assoc [rec k v]
|
||||
# assoc on a known key keeps the layout; a new key falls back to a struct
|
||||
(def desc (in rec 0))
|
||||
(def pos (get (desc :idx) k))
|
||||
(if (nil? pos)
|
||||
(let [t @{}] (each kk (desc :jolt/shape) (put t kk (shape-get rec kk nil))) (put t k v) (table/to-struct t))
|
||||
(let [a (array ;rec)] (put a (+ pos 1) v) (tuple ;a))))
|
||||
|
||||
(defn core-get [m k &opt default]
|
||||
(default default nil)
|
||||
(if (nil? m) default
|
||||
# inline the shape check (no fn call) so non-shape gets pay only a tuple? test
|
||||
(if (and (tuple? m) (> (length m) 0) (struct? (in m 0)) (not (nil? (in (in m 0) :jolt/shape))))
|
||||
(shape-get m k default)
|
||||
(if (core-sorted? m) ((sorted-op m :get) m k default)
|
||||
(if (core-transient? m)
|
||||
(case (m :kind)
|
||||
|
|
@ -583,7 +622,7 @@
|
|||
# (get "a:b" 1) was nil.
|
||||
(if (and (or (string? m) (buffer? m)) (number? k) (>= k 0) (< k (length m)))
|
||||
(make-char (in m k))
|
||||
default))))))))))
|
||||
default)))))))))))
|
||||
|
||||
# Runtime invoke dispatch for COMPILED code (interpreter uses evaluator's
|
||||
# jolt-invoke). Handles real functions plus Clojure IFn collections.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue