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:
Yogthos 2026-06-12 17:45:18 -04:00
parent e41832c05d
commit c4be5d8a0e
4 changed files with 89 additions and 13 deletions

View file

@ -0,0 +1,44 @@
# ^:struct type hint (jolt-dad). A constant-keyword lookup on a local hinted
# ^:struct skips the :jolt/type guard and emits a bare get (~20ns vs ~36ns),
# the way Clojure type hints let the compiler specialize. The hint is a
# programmer assertion (a lie just makes the raw get return the wrong thing,
# same contract as ^String); these tests pin that an ACCURATE hint is
# correctness-preserving, that it drops the guard, and that it survives inlining.
(import ../../src/jolt/api :as api)
(import ../../src/jolt/backend :as backend)
(import ../../src/jolt/reader :as reader)
(print "Struct hint (jolt-dad)...")
(os/setenv "JOLT_DIRECT_LINK" "1") # inline on, so hint-through-inline is exercised
(def ctx (api/init {:compile? true}))
(api/eval-string ctx "(ns sh)")
(each s ["(defn v3 [r g b] {:r r :g g :b b})"
"(defn dot [^:struct l ^:struct r] (+ (+ (* (:r l) (:r r)) (* (:g l) (:g r))) (* (:b l) (:b r))))"
"(defn sub [^:struct l ^:struct r] {:r (- (:r l) (:r r)) :g (- (:g l) (:g r)) :b (- (:b l) (:b r))})"
"(defn lensq [^:struct v] (dot v v))"]
(api/eval-string ctx s))
(defn guards [src]
(def code (string/format "%p" (backend/emit-ir ctx (backend/analyze-form ctx (reader/parse-string src)))))
(length (string/find-all ":jolt/type" code)))
# the guard is dropped for hinted subjects, kept for unhinted ones
(assert (= 1 (guards "(fn [v] (:r v))")) "unhinted (:r v) keeps the guard")
(assert (= 0 (guards "(fn [^:struct v] (:r v))")) "hinted (:r v) drops the guard")
(assert (= 0 (guards "(fn [^:struct v] (+ (+ (:r v) (:g v)) (:b v)))")) "all three hinted lookups bare")
(assert (= 0 (guards "(fn [^:struct v] (lensq v))")) "hint survives through an inlined call")
# accurate hints are correctness-preserving (value identical to the guarded path)
(assert (= 32 (api/eval-string ctx "(dot (v3 1 2 3) (v3 4 5 6))")) "hinted dot value")
(assert (= 14 (api/eval-string ctx "(lensq (v3 1 2 3))")) "hinted lensq (inline-flow) value")
(assert (= 7 (api/eval-string ctx "(:r (sub (v3 9 8 7) (v3 2 0 0)))")) "hinted sub field")
# a hinted value flowing through an inlined call still reads correctly
(api/eval-string ctx "(defn hit [^:struct ray ^:struct c] (lensq (sub (:origin ray) c)))")
(assert (= 48 (api/eval-string ctx "(hit {:origin (v3 5 5 5) :direction (v3 0 0 0)} (v3 1 1 1))"))
"hinted value through nested inline reads correctly")
# a missing key on a hinted struct still reads nil (struct miss), like a guarded get
(assert (= nil (api/eval-string ctx "((fn [^:struct m] (:absent m)) (v3 1 2 3))")) "hinted struct miss -> nil")
(print "Struct hint passed!")