A devirtualized protocol call resolved its impl with devirt-resolve on EVERY call — but the tag/proto/method are compile-time constants, so the resolved fn is a runtime constant (closed world). That per-call find-protocol-method (three hashtable lookups) was the cost: on mono-dispatch, dispatch was ~75% of the time (ablation: same arithmetic direct-call 166ms vs dispatch 673ms). Resolve once. When emitting a direct-link def, each devirt site gets a fresh cache cell, bound to #f in a let wrapping the def (so it persists across calls and is shared by every invocation); the site resolves into it on first use ((or cell (let ((_f (devirt-resolve ..))) (set! cell _f) _f))) and reuses it after — the inline cache the JVM gets for free. First call still passes the real receiver, so the Object/host-tag fallback (devirt-resolve) is unchanged. mono-dispatch 673ms -> 214ms (~3.15x), 47.5x -> ~15x JVM, near the 166ms direct-call floor. run-devirt.ss gains the cached-path checks (cell present, 1st call caches + 2nd reuses, both == dispatch). make test / shakesmoke green, selfhost holds, 0 new divergences. Co-authored-by: Yogthos <yogthos@gmail.com>
104 lines
5.5 KiB
Scheme
104 lines
5.5 KiB
Scheme
;; run-devirt.ss — protocol-call devirtualization gate (backend_scheme emit).
|
|
;;
|
|
;; The inference annotates a monomorphic protocol call with :devirt-type/-proto/
|
|
;; -method (jolt.passes.types); the back end then resolves the impl by that static
|
|
;; tag. This gate pins both halves: the emitted form uses find-protocol-method, and
|
|
;; evaluating it returns the same value the ordinary dispatch would — for a record's
|
|
;; inline impl, an extend-type impl, and across distinct receiver types.
|
|
;;
|
|
;; chez --script host/chez/run-devirt.ss
|
|
(import (chezscheme))
|
|
(load "host/chez/rt.ss")
|
|
(set-chez-ns! "clojure.core")
|
|
(load "host/chez/seed/prelude.ss")
|
|
(load "host/chez/post-prelude.ss")
|
|
(set-chez-ns! "user")
|
|
(load "host/chez/host-contract.ss")
|
|
(load "host/chez/seed/image.ss")
|
|
(load "host/chez/compile-eval.ss")
|
|
|
|
(define analyze (var-deref "jolt.analyzer" "analyze"))
|
|
(define emit (var-deref "jolt.backend-scheme" "emit"))
|
|
(define kw (lambda (n) (keyword #f n)))
|
|
|
|
(define (evals src) (jolt-compile-eval (string-append "(do " src ")") "user"))
|
|
;; define two record types implementing one protocol — Circle via an inline impl,
|
|
;; Square via extend-type — plus instances to dispatch on.
|
|
(evals "(defprotocol Shape (area [s]))")
|
|
(evals "(defrecord Circle [r] Shape (area [s] (:r s)))")
|
|
(evals "(defrecord Square [w])")
|
|
(evals "(extend-type Square Shape (area [s] (* (:w s) (:w s))))")
|
|
(evals "(def c (->Circle 7))")
|
|
(evals "(def sq (->Square 5))")
|
|
|
|
;; analyze (area RECV), annotate it as a devirt call on `type`, and emit. RECV is a
|
|
;; var name (c/sq) the emitted code resolves at eval time.
|
|
(define (devirt-emit type recv)
|
|
(let* ((ir (analyze (make-analyze-ctx "user") (jolt-ce-read (string-append "(area " recv ")"))))
|
|
(dv (jolt-assoc ir (kw "devirt-type") type (kw "devirt-proto") "Shape"
|
|
(kw "devirt-method") "area")))
|
|
(emit dv)))
|
|
|
|
(define fails 0) (define total 0)
|
|
(define (check label actual expected)
|
|
(set! total (+ total 1))
|
|
(unless (equal? actual expected)
|
|
(set! fails (+ fails 1))
|
|
(printf " FAIL ~a: got ~s expected ~s\n" label actual expected)))
|
|
(define (has-sub? s sub)
|
|
(let ((n (string-length s)) (m (string-length sub)))
|
|
(let loop ((i 0)) (cond ((> (+ i m) n) #f)
|
|
((string=? (substring s i (+ i m)) sub) #t)
|
|
(else (loop (+ i 1)))))))
|
|
;; eval an emitted Scheme string in the loaded runtime (var-deref resolves c/sq).
|
|
(define (run-emit scm) (eval (read (open-input-string scm)) (interaction-environment)))
|
|
|
|
(let ((e (devirt-emit "user.Circle" "c")))
|
|
(check "emit uses devirt-resolve" (has-sub? e "devirt-resolve") #t)
|
|
(check "devirt inline impl == dispatch" (run-emit e) (evals "(area c)"))) ; 7
|
|
|
|
(let ((e (devirt-emit "user.Square" "sq")))
|
|
(check "devirt extend-type impl == dispatch" (run-emit e) (evals "(area sq)"))) ; 25
|
|
|
|
;; a normal (no devirt) call still goes through dispatch and agrees — the path the
|
|
;; megamorphic / unknown-receiver site keeps.
|
|
(let ((e (emit (analyze (make-analyze-ctx "user") (jolt-ce-read "(area c)")))))
|
|
(check "non-devirt path no devirt-resolve" (has-sub? e "devirt-resolve") #f)
|
|
(check "non-devirt still dispatches" (run-emit e) 7))
|
|
|
|
;; a record that relies on the protocol's Object default (no direct impl): the
|
|
;; inference still types it as a concrete record and annotates devirt, so the
|
|
;; emitted call must resolve the same value dispatch would. find-protocol-method
|
|
;; on the record's own tag misses here, so the devirt path has to fall back to
|
|
;; ordinary dispatch (else it applies #f and crashes).
|
|
(evals "(extend-protocol Shape Object (area [s] :obj-default))")
|
|
(evals "(defrecord Plain [n])")
|
|
(evals "(def pl (->Plain 9))")
|
|
(let ((e (devirt-emit "user.Plain" "pl")))
|
|
(check "devirt Object-default == dispatch" (run-emit e) (evals "(area pl)"))) ; :obj-default
|
|
|
|
;; in a direct-link build a devirt site caches the resolved impl in a per-site cell
|
|
;; (resolved once, reused) instead of resolving per call. Annotate the (area x) in a
|
|
;; def body and emit the top form; the result must carry the cell and still be right.
|
|
(let* ((set-direct-link! (var-deref "jolt.backend-scheme" "set-direct-link!"))
|
|
(emit-top-form (var-deref "jolt.backend-scheme" "emit-top-form"))
|
|
(dn (analyze (make-analyze-ctx "user") (jolt-ce-read "(def usearea (fn [x] (area x)))")))
|
|
(ar0 (jolt-nth (jolt-get (jolt-get dn (kw "init")) (kw "arities")) 0))
|
|
(inv (jolt-get ar0 (kw "body")))
|
|
(inv2 (jolt-assoc inv (kw "devirt-type") "user.Circle" (kw "devirt-proto") "Shape" (kw "devirt-method") "area"))
|
|
(dn2 (jolt-assoc dn (kw "init")
|
|
(jolt-assoc (jolt-get dn (kw "init")) (kw "arities")
|
|
(jolt-vector (jolt-assoc ar0 (kw "body") inv2))))))
|
|
(set-direct-link! #t)
|
|
(let ((e (emit-top-form dn2)))
|
|
(set-direct-link! #f)
|
|
(check "devirt in a def caches in a per-site cell" (has-sub? e "_dvc$") #t)
|
|
(check "cached cell still resolves the impl" (has-sub? e "devirt-resolve") #t)
|
|
;; eval the def, then call it: caches on first call, reuses after — still 7.
|
|
(run-emit e)
|
|
(check "cached devirt == dispatch (1st call)" (jolt-invoke (var-deref "user" "usearea") (var-deref "user" "c")) 7)
|
|
(check "cached devirt == dispatch (2nd call, from cell)" (jolt-invoke (var-deref "user" "usearea") (var-deref "user" "c")) 7)))
|
|
|
|
(if (= fails 0)
|
|
(begin (printf "devirt gate: ~a/~a passed\n" total total) (exit 0))
|
|
(begin (printf "devirt gate: ~a/~a passed (~a failed)\n" (- total fails) total fails) (exit 1)))
|