From 639effd58c1e635ce2d54079c4a7010982f366a9 Mon Sep 17 00:00:00 2001 From: Yogthos Date: Wed, 10 Jun 2026 16:42:46 -0400 Subject: [PATCH] backend: inline the var deref at indirect call sites (fib 1.75x) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/jolt/backend.janet | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/jolt/backend.janet b/src/jolt/backend.janet index 5abc253..7cea92c 100644 --- a/src/jolt/backend.janet +++ b/src/jolt/backend.janet @@ -286,10 +286,25 @@ :var (let [cell (cell-for ctx (node :ns) (node :name))] (if (direct-var? ctx cell) (cell :root) # direct link: embed the fn value - # Indirect: live deref. Quote the cell 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). - (tuple var-get (tuple 'quote cell)))) + # Indirect: live deref, with the var-get FN CALL inlined away + # (jolt-8sq): a non-dynamic var's value is always its root, so + # the common case is two native table ops + a branch instead of + # 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 # reference. binding keys its thread-binding frame on this exact cell. :the-var (tuple 'quote (cell-for ctx (node :ns) (node :name)))