Skip to content

jit: hot-path wins, [jit] in a C++ AOT host, and unresolved AOT-object address globals - #3917

Open
aleksisch wants to merge 4 commits into
masterfrom
aleksisch/jit-runtime-host
Open

jit: hot-path wins, [jit] in a C++ AOT host, and unresolved AOT-object address globals#3917
aleksisch wants to merge 4 commits into
masterfrom
aleksisch/jit-runtime-host

Conversation

@aleksisch

@aleksisch aleksisch commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

ABI break: Context grows 256 bytes - every embedder and prebuilt shared_module rebuilds. No baked offset moves, so warm JIT caches stay valid.

Four changes to the LLVM JIT backend, from profiling a game host that runs daslang through C++ AOT with the runtime JIT enabled beside it.

Two are emitted-code wins. The 32-bit integer remainder was lowered as a sitofp, a double divide, an fptosi, a multiply and a subtract, where every other width already emitted srem - roughly 20 cycles of divide latency instead of the multiply-and-shift LLVM picks for a constant divisor. The guards that made the float path safe, the divide-by-zero check and the INT_MIN % -1 rewrite, already run before the emitter reaches that point, so they cover the native form too. Separately, jitted code resolved a block's annotation data by sid on every call - once per tick for an entity system, 212 cycles each - recomputing a value that cannot change. A 16-slot direct-mapped memo answers the repeats, keyed on the map it was filled from so a rebuilt tabAdLookup invalidates it without a cooperating caller. It sits last in Context, so the offsets the emitter bakes into every jitted function do not move.

The third makes [jit] usable in a host that ships C++ AOT. run_jit returned early for any AOT program, and AOT emitted a direct C++ call that could never reach a jitted body. The early return now respects jit_enabled; selection narrows to what linkCppAot left unbound, since it runs before the simulate macros and SimFunction.aot marks the covered set; and a [jit] function takes the hybrid call form that dispatches aot, then jit, then interpreted. The fourth catches offline-AOT-object address globals that nothing fills at load: LLVM folds their loads to null, proves the body UB and reduces it to a zero-length unreachable sharing an address with the next function, so das_aot_register binds a hash to alien code. It found one live case, ascend-new of a handled type.

Where to look: Context::AdMemo and adBySidMemo in simulate.h; jit_selects in llvm_jit_run.das; collect_unresolved_address_globals in llvm_aot.das.

Validation, claims, ledger

Validation

  • The dasLLVM module-owned suite (--test modules/dasLLVM/tests) passes. It sits outside the core tests/ sweep, so no CI lane covers it, and this branch is mostly dasLLVM.
  • das2rst regenerates with zero changes and zero Uncategorized: the new is_aot_function binding joins the hidden Jit group beside is_jit_function. No .rst differs from master, so sphinx-build was not re-run.
  • Context offsets measured against master: stopFlags stays at 640, sizeof goes 816 -> 1072.
  • The per-tick numbers are from profiling an enlisted act-stage ES, not from this repo's suite, and are not reproducible in CI.

Claims - stated, not tested

  • An AOT host jits only what the AOT did not cover. Nothing in this repository sets policies.aot and policies.jit_enabled together, so the aot_host arm of jit_selects has no in-tree coverage. Verified by reading the ordering: linkCppAot runs before the simulate macros, so SimFunction.aot is authoritative when run_jit reads it. A break shows as an AOT host either putting the whole program through codegen at load, or jitting nothing.
  • The address-global scan is scoped to the DllName.glob() name space. That path runs only under emit_aot_object, and no in-tree test builds an AOT object carrying a stray zero global. Verified by enumerating every zero-initialized global the emitter creates: the three address-global creators all name through glob(), and the two that do not (fileInfo, the wasm field-offset global) are not address slots. A das module-scope global is not an LLVM global at all - it lives in the Context globals block - so it cannot reach this check.
  • extraOffset on PtrSlots. int? has no fields, so no site folds a nonzero offset onto this annotation and the plumbing cannot be exercised. Carried because it is part of the simulateGetAt contract and this annotation is the shape embedders copy.

Not done

  • adBySidMemo is not concurrency-safe. It replaces a read-only map lookup with two array writes, so two threads driving one Context can match a sid against the previous occupant's value. A Context is single-threaded by contract.
  • LLVMBuildSRemInt32 and LLVMBuildURemUInt32 are removed from llvm_boost with no deprecation shim. In-tree callers are gone; an out-of-tree consumer calling either stops compiling.

🤖 Generated with Claude Code

Churkin Aleksey and others added 4 commits September 1, 2026 15:29
Every annotation in tree yields a ref from makeIndexType, so nothing reached
the jit's at`handle path for a pointer result - the shape an embedder's keyed
container has. dasUnitTest gains PtrSlots, indexed by string, yielding `int?`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An address global nothing fills at load is not merely unresolved: LLVM folds
its loads to null, proves the body UB and drops it to a zero-length
`unreachable` that shares an address with the next function, so
das_aot_register binds that hash to alien code.

collect_unresolved_address_globals reports them and run_jit panics before the
optimizer erases the evidence. It found one live case: ascend-new of a handled
type, which the emitter names after the ascend node while CollectExternVisitor
visited only ExprNew.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
32-bit % was lowered as a sitofp, a double divide, an fptosi, a multiply and a
subtract, where every other width already emitted srem. The guards that made
the float path safe - the divide-by-zero check and the INT_MIN % -1 rewrite -
run before the emitter, so they cover the native form too. On an enlisted ES
dominated by `x % 4093`: 131 -> 53 us/tick, against 45 for C++ AOT.

Jitted code also resolved a block's annotation data by sid on every call, 212
cycles each. A 16-slot direct-mapped memo answers the repeats; it is keyed on
the map it was filled from, so a rebuilt tabAdLookup invalidates it without a
cooperating caller. Act-stage das per tick: 191.6 -> 180.9 us.

Context grows 256 bytes. The memo sits last so no baked offset moves, and the
DLL cache key now folds the Context offsets the emitter bakes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
run_jit returned early for any AOT program, and AOT emitted a direct C++ call
that could never reach a jitted body.

The early return now respects jit_enabled. Selection narrows to what linkCppAot
left unbound - it runs before the simulate macros, so SimFunction.aot marks the
covered set - plus anything marked [jit]; jitting the covered set would displace
a native body at full codegen cost per load. A [jit] function takes the hybrid
call form, which dispatches aot, then jit, then interpreted, and is what makes a
re-jit visible to callers.

The backend's own daslib modules opt out of the host's script policies: a game
sets no_global_variables, which they legitimately trip.

Verified in a game host built with C++ AOT plus the JIT runtime.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@aleksisch
aleksisch force-pushed the aleksisch/jit-runtime-host branch from 7fde9c3 to e7a5fbf Compare September 1, 2026 13:14
@aleksisch aleksisch changed the title jit: make [jit] real in a host, next to C++ AOT (+ four jit fixes) jit: hot-path wins, [jit] in a C++ AOT host, and unresolved AOT-object address globals Sep 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant