backend: inline the var deref at indirect call sites (fib 1.75x)
An indirect global reference emitted ((var-get 'cell) ...) — a function call per deref, whose body is a binding-stack check plus a root read. The emitter now inlines that: (if (in 'cell :dynamic) (var-get 'cell) (in 'cell :root)). Non-dynamic vars — the vast majority of references — pay two native table ops and a branch instead of a call; dynamic vars take the full var-get (thread-binding walk). Redefinition stays live (:root is read per call) and binding semantics are exact: the :dynamic check is PER CALL, not at emit, because a (def ^:dynamic x) in the same compiled unit marks the cell dynamic only when the def runs — the same reason JVM Clojure's Var.deref() checks the thread-bound bit every call (an emit-time variant was 1.7x faster still on fib but failed conformance exactly there). fib 130 -> 74 ms (1.75x); bench TOTAL 4564 -> 4437 back-to-back. This displaces the gen-counter inline-cache design from jolt-8sq: with var-get's existing fast path, resolution was never the cost — the call was. A gen-guarded cache would add state per site to save nothing further, and couldn't skip the dynamic check anyway. Found while benching: map-read regressed ~7x back in batch 2 (canonical zipmap builds a phm where kvs->map built a struct) — filed as jolt-s3y. Gate: jpm test exit 0, conformance 326x3, suite >= baseline.
This commit is contained in:
parent
19d59cba85
commit
639effd58c
1 changed files with 19 additions and 4 deletions
|
|
@ -286,10 +286,25 @@
|
||||||
:var (let [cell (cell-for ctx (node :ns) (node :name))]
|
:var (let [cell (cell-for ctx (node :ns) (node :name))]
|
||||||
(if (direct-var? ctx cell)
|
(if (direct-var? ctx cell)
|
||||||
(cell :root) # direct link: embed the fn value
|
(cell :root) # direct link: embed the fn value
|
||||||
# Indirect: live deref. Quote the cell so it's embedded by
|
# Indirect: live deref, with the var-get FN CALL inlined away
|
||||||
# reference (a bare table in arg position would be re-evaluated as
|
# (jolt-8sq): a non-dynamic var's value is always its root, so
|
||||||
# a constructor — deep-copying it, and any atom in :root, each call).
|
# the common case is two native table ops + a branch instead of
|
||||||
(tuple var-get (tuple 'quote cell))))
|
# a function call. Dynamic vars take the full var-get (thread-
|
||||||
|
# binding walk). The cell is quoted so it's embedded by
|
||||||
|
# reference (a bare table in arg position would be re-evaluated
|
||||||
|
# as a constructor — deep-copying it, and any atom in :root,
|
||||||
|
# each call). Redefinition stays live: :root is read per call.
|
||||||
|
# The :dynamic check must be PER CALL, not at emit: a
|
||||||
|
# (def ^:dynamic x) in the same compiled unit marks the cell
|
||||||
|
# dynamic only when the def RUNS, after this site was emitted —
|
||||||
|
# the same reason JVM Clojure's Var.deref() checks the
|
||||||
|
# thread-bound bit on every call. Non-dynamic vars (the vast
|
||||||
|
# majority) pay two native table ops + a branch instead of a
|
||||||
|
# function call.
|
||||||
|
(let [qcell (tuple 'quote cell)]
|
||||||
|
['if ['in qcell :dynamic]
|
||||||
|
(tuple var-get qcell)
|
||||||
|
['in qcell :root]])))
|
||||||
# (var x): the var object itself (not its value) — the embedded cell, by
|
# (var x): the var object itself (not its value) — the embedded cell, by
|
||||||
# reference. binding keys its thread-binding frame on this exact cell.
|
# reference. binding keys its thread-binding frame on this exact cell.
|
||||||
:the-var (tuple 'quote (cell-for ctx (node :ns) (node :name)))
|
:the-var (tuple 'quote (cell-for ctx (node :ns) (node :name)))
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue