Fast record field reads: single eq? scan, skip the get-arm walk

(:field rec) / (get rec :field) lowers to (jolt-get rec kw), which walked the
get-arm list to reach the jrec arm, then did jrec-has? + jrec-lookup — TWO linear
scans, each comparing keys through the generic jolt=2 equality dispatcher. Field
keys are interned keywords, so:

- jrec-key=? compares a keyword query by eq? (jolt=2 only for non-keyword keys),
- jrec-ref does ONE scan (vs has?+lookup) and runs a deftype's ILookup valAt only
  when the field is genuinely absent (present-nil still returns nil, not default),
- jolt-get-dispatch checks jrec? first, skipping the get-arm walk for the hottest
  get target. jrec-lookup/jrec-has? (used by =, contains?, etc.) get the fast
  compare too.

binary-trees 135x->18.9x, dispatch 121x->26.4x, mono-dispatch 327x->108x vs JVM.
Runtime .ss (collections.ss + records.ss), no re-mint; make test + shakesmoke +
buildsmoke green, record get/assoc/keys/=/count semantics unchanged.
This commit is contained in:
Yogthos 2026-06-26 00:52:34 -04:00
parent 8ad4e1cf31
commit 707737d989
2 changed files with 32 additions and 13 deletions

View file

@ -239,11 +239,16 @@
((string? coll) (let ((i (->idx k)))
(if (and (fixnum? i) (fx>=? i 0) (fx<? i (string-length coll))) (string-ref coll i) d)))
(else d)))
;; jrec? / jrec-ref live in records.ss (loaded later); these are forward references
;; resolved at call time. A record field read is the hottest get, so check it first
;; and skip the get-arm walk.
(define (jolt-get-dispatch coll k d)
(let loop ((as jolt-get-arms))
(cond ((null? as) (jolt-get-base coll k d))
(((caar as) coll) ((cdar as) coll k d))
(else (loop (cdr as))))))
(if (jrec? coll)
(jrec-ref coll k d)
(let loop ((as jolt-get-arms))
(cond ((null? as) (jolt-get-base coll k d))
(((caar as) coll) ((cdar as) coll k d))
(else (loop (cdr as)))))))
(define jolt-get
(case-lambda
((coll k) (jolt-get-dispatch coll k jolt-nil))

View file

@ -16,16 +16,32 @@
(define-record-type jrec (fields tag pairs) (nongenerative chez-jrec-v1))
(define jolt-deftype-kw (keyword "jolt" "deftype"))
;; Field keys are interned keywords, so a keyword query compares by eq? (the hot
;; (:field rec) / (get rec :field) case); any other key type falls back to jolt=2.
(define (jrec-key=? field-key k)
(if (keyword? k) (eq? field-key k) (jolt=2 field-key k)))
(define (jrec-lookup r k d)
(if (jolt=2 k jolt-deftype-kw)
(if (eq? k jolt-deftype-kw)
(jrec-tag r)
(let loop ((ps (jrec-pairs r)))
(cond ((null? ps) d)
((jolt=2 (caar ps) k) (cdar ps))
((jrec-key=? (caar ps) k) (cdar ps))
(else (loop (cdr ps)))))))
(define (jrec-has? r k)
(let loop ((ps (jrec-pairs r)))
(cond ((null? ps) #f) ((jolt=2 (caar ps) k) #t) (else (loop (cdr ps))))))
(cond ((null? ps) #f) ((jrec-key=? (caar ps) k) #t) (else (loop (cdr ps))))))
;; The get path: one scan (vs jrec-has? + jrec-lookup = two); a deftype's ILookup
;; valAt runs only when the field is genuinely missing.
(define (jrec-ref coll k d)
(if (eq? k jolt-deftype-kw)
(jrec-tag coll)
(let loop ((ps (jrec-pairs coll)))
(cond ((null? ps)
(cond ((find-method-any-protocol (jrec-tag coll) "valAt")
=> (lambda (m) (jolt-invoke m coll k d)))
(else d)))
((jrec-key=? (caar ps) k) (cdar ps))
(else (loop (cdr ps)))))))
;; mutate a deftype's mutable field in place: the pairs are runtime cons cells,
;; so set-cdr! updates the field. (set! field v) inside a method
;; lowers to this; returns v, as set! does.
@ -77,12 +93,10 @@
;; compiled to (get inst :field), never recurse); a NON-field key on a deftype that
;; implements clojure.lang.ILookup routes to its valAt (core.match's pattern types
;; compute ::tag in valAt), else the default.
(register-get-arm! jrec?
(lambda (coll k d)
(cond ((jrec-has? coll k) (jrec-lookup coll k d))
((find-method-any-protocol (jrec-tag coll) "valAt")
=> (lambda (m) (jolt-invoke m coll k d)))
(else d))))
;; jrec is the hottest get target (every record field read); jolt-get-dispatch
;; (collections.ss) checks jrec? directly and calls jrec-ref, skipping the get-arm
;; walk. This registration is the equivalent fallback for any other caller.
(register-get-arm! jrec? jrec-ref)
;; A jrec is a defrecord (map of fields) by default, BUT a deftype that
;; implements a clojure.lang collection interface carries the op as an inline
;; method — prefer that method, else fall back to the field/map behavior. (jrec-cl