Skip to content

codegen: forward inferred effects to LLVM call-site attributes - #63464

Open
vchuravy wants to merge 1 commit into
masterfrom
vc/effects-3-effects-attrs
Open

vchuravy wants to merge 1 commit into
masterfrom
vc/effects-3-effects-attrs

Conversation

@vchuravy

@vchuravy vchuravy commented Sep 28, 2026 •

Copy link
Copy Markdown
Member

Third of a 4-PR stack re-landing #61394 (reverted in #62697 because of #62613): #63462 → #63463 → #63464 → #63465. This is the PR that needs the design discussion and PkgEval; the two below it are the safety nets it relies on.

What it does

Translates a callee's ipo_purity_bits into attributes on specsig call sites so LICM, GVN and DSE can use what inference already proved:

nothrow              -> nounwind
terminates           -> mustprogress
nothrow + terminates -> willreturn
notaskstate          -> readnone on the gcstack argument

consistent           effect_free    memory(...)
ALWAYS_TRUE          ALWAYS_TRUE    argmem: read
ALWAYS_TRUE          IF_INACCESS.   argmem: read, inaccessiblemem: readwrite   (only with :inaccessiblememonly)
IF_NOTRETURNED       as above       ... plus inaccessiblemem: readwrite
otherwise                           (no claim)

Out-parameters (sret, return_roots, and the Union calling convention's result buffer, which carries no sret attribute and is recognized via returninfo.cc) add argmem: write; the remaining pointer parameters of an :effect_free callee get readonly.

Example: A[i] = poly(x) with @noinline poly over 1e7 elements goes from 17 ms to 4 ms (call hoisted, loop vectorized); fib(n-1) + fib(n-2) style duplicate calls are CSE'd.

How the objections from #62697 / #62613 are addressed

Why :consistent is required (learned the hard way on CI)

Julia's effects have no notion of synchronization. An atomic load is :effect_free, so a callee that spin-waits on @atomic x.done is (+e, ?m) like any other reader of mutable memory, but LLVM would hoist a readonly call out of the loop and forward loads across it — neither is legal across an acquire. :consistent (ALWAYS_TRUE) means the result cannot depend on mutable memory, so no atomic load can be involved. For the same reason writers are never described: a release store is (+c, ?e, ?m) and must keep every preceding store above it.

The first revision of this PR keyed the mapping off :inaccessiblememonly alone (so ?c readers got memory(read)/memory(argmem: read)); it livelocked threads_exec.jl with JULIA_NUM_THREADS=2,0 on x86_64-linux-gnu — both threads at 100 % in trypoptask → multiq_deletemin, one task stuck in Channel put!, the other in take! — i.e. a stale atomic read in the scheduler/channel path. Timing-dependent (the same code passed on #63465's run), so no local test catches it deterministically; the fix is structural.

Two further refinements:

  • CONSISTENT_IF_NOTRETURNED callees return a fresh mutable object, so two calls must never be merged (GVN/EarlyCSE value-number readonly calls). They get an extra inaccessiblemem: readwrite, which keeps load/store forwarding across the call but makes it neither mergeable nor hoistable.
  • The Union calling convention's result buffer is the first argument but carries no sret attribute, so out-parameters are derived from returninfo.cc. (Missing this produced garbage results during bootstrap in an earlier revision.)

Safety valve

JULIA_LLVM_ARGS=-julia-effects-attrs=0 disables the translation, so a miscompile found in the field can be worked around without a revert. The mapping and its invariants are documented in doc/src/devdocs/llvm.md.

Testing

  • make testall locally (x86_64-linux, assertions on): all sets green except three Pkg/test/extensions.jl coverage assertions that fail identically on unmodified master on this machine.
  • WITH_GC_VERIFY=1 build (assertions on): sysimage + all stdlib pkgimages build clean, and gc core strings threads channels misc (11.3M assertions) plus Compiler/{codegen,effects} pass with no GC verification errors.
  • test/llvmpasses lit suite 56/56, incl. a new end-to-end rooting test (safepoint-effects-rooting.jl).
  • New Compiler/test/codegen.jl cases for every row of the table, sret/Union out-params, readonly parameters, the fresh-allocation no-CSE case, a @noinline atomic-load spin-wait (call must stay in the loop, no memory claim), LICM hoisting and the Base.blackbox negative.
  • The add_fn_attrs_for_effects marks calls that can write memory as memory(read: argmem) #62613 example and a barrier/store stress under GC.gc(false).
  • After the :consistent fix: threads test set 3× green locally.

Not yet run on this revision: nanosoldier benchmarks / PkgEval (the in-flight gh-5884395480 run on #63465 covers the previous revision and only compares #63465 against this PR's branch), Windows/i686 CI.

Follow-ups this motivates

  • A :nosync-like effect (or per-argument read/write in EscapeAnalysis, cf. EscapeAnalysis: track address observation by the GC, and capture by finalizers #62870) would let ?c readers like length(v)/r[] get memory(read)/argmem: read safely — today :consistent is the only proxy for "no synchronization".
  • A fresh-allocation / noalias-return fact so CONSISTENT_IF_NOTRETURNED callees keep the readonly claim without the CSE hazard.

🤖 Generated with Claude Code

@vchuravy

Copy link
Copy Markdown
Member Author

The test x86_64-linux-gnu failure on build 3090 was real, and it's fixed in the force-push (fed5486 → 94f0530).

Symptom: threads_exec.jl with JULIA_NUM_THREADS=2,0 livelocked and was killed by the thread-test watchdog after ~20 min. The SIGUSR1 profile shows both threads at 100 % in wait → trypoptask → multiq_deletemin, one task blocked in Channel put!, the other in take! — the scheduler never sees the runnable task.

Cause: the first revision keyed the memory mapping off :inaccessiblememonly alone, so (+e, ?c, ?m) callees got memory(read) / memory(argmem: read). Julia's effects have no synchronization bit: an atomic load is :effect_free, so an acquire load inside such a callee was invisible to LLVM, which is then free to hoist the call out of a loop or forward loads across it. (@atomic :acquire f.done, Threads.Atomic{Int}[] and Base.Event's @atomic e.set are all (?c,+e,?m); the write side is the same story, release stores being (+c,?e,?m).)

Fix: memory claims only when :consistent proves the result doesn't depend on mutable memory (ALWAYS_TRUE or CONSISTENT_IF_NOTRETURNED), and no write claims at all — i.e. back to the #61394 gate, keeping the out-parameter/Union/readonly refinements. The threads set passes 3× locally on the fixed build; the loss is that ?c readers like length(v) get no memory claim until inference can express "does not synchronize". Devdocs and PR description updated accordingly.

🤖 Generated with Claude Code

@vchuravy
vchuravy force-pushed the vc/effects-3-effects-attrs branch 2 times, most recently from 0b92948 to d89f4b9 Compare October 5, 2026 05:40
Base automatically changed from vc/effects-2-barrier-argmem to master October 5, 2026 10:27
Translate a callee's `ipo_purity_bits` into attributes on specsig call
sites so that LICM, GVN and DSE can use what inference already proved:

  nothrow              -> nounwind
  terminates           -> mustprogress
  nothrow + terminates -> willreturn
  notaskstate          -> readnone on the gcstack argument

  consistent           effect_free    memory(...)
  ALWAYS_TRUE          ALWAYS_TRUE    argmem: read
  ALWAYS_TRUE          IF_INACCESS.   argmem: read, inaccessiblemem: rw
                                      (only with :inaccessiblememonly)
  IF_NOTRETURNED       as above       ... plus inaccessiblemem: rw
  otherwise                           (no claim)

Out-parameters (sret, return_roots, and the Union calling convention's
result buffer, which carries no `sret` attribute and is therefore
recognized via `returninfo.cc`) add `argmem: write`; the remaining
pointer parameters of an :effect_free callee get `readonly`.

Re-lands #61394 (reverted in #62697 because of #62613) with the GC
interactions handled explicitly instead of by pass ordering:

- Attributes go on the call site only, never on the callee's
  declaration, so nothing has to be undone at module scope.
- Every such call site is tagged "julia.safepoint", which
  LateLowerGCFrame uses to keep rooting and to strip the effects.
- readnone on the gcstack argument is only emitted for :notaskstate
  callees; dynamic dispatch, current_scope, current_task and ccall all
  clear that bit, so such a callee cannot observe world age or scope
  through pgcstack.
- WeakRef.value loads and finalizer side effects may be reordered
  across such a call; both are already permitted at the Julia level
  for :effect_free callees.

Why :consistent is required for any memory claim: Julia's effects have
no notion of synchronization. An atomic load is :effect_free, so a
callee that spin-waits on `@atomic x.done` is (+e, ?m) like any other
reader of mutable memory, but LLVM would hoist a readonly call out of
the loop and forward loads across it, neither of which is legal across
an acquire. :consistent (ALWAYS_TRUE) means the result cannot depend on
mutable memory, so no atomic load can be involved. For the same reason
writers are never described: a release store is (+c, ?e, ?m) and must
keep every preceding store above it. (An earlier revision of this
change keyed the mapping off :inaccessiblememonly alone and livelocked
threads_exec.jl in the scheduler on CI.)

CONSISTENT_IF_NOTRETURNED callees return a fresh mutable object, so two
calls must never be merged (GVN/EarlyCSE value-number readonly calls);
they get an extra `inaccessiblemem: readwrite`, which keeps load/store
forwarding across the call but makes it neither mergeable nor
hoistable.

The translation can be disabled with
`JULIA_LLVM_ARGS=-julia-effects-attrs=0`; the mapping and its invariants
are documented in doc/src/devdocs/llvm.md.

Assisted-by: Claude Code (Fable 5.1)
@vchuravy
vchuravy force-pushed the vc/effects-3-effects-attrs branch from d89f4b9 to e7bf730 Compare October 5, 2026 10:27
@vchuravy
vchuravy marked this pull request as ready for review October 5, 2026 10:27
@vchuravy
vchuravy requested review from gbaraldi and vtjnash October 5, 2026 10:29
@Keno

Keno commented Oct 8, 2026

Copy link
Copy Markdown
Member

Seems in principle sound to me, although this stuff is very tricky. Definitely needs a pkgeval.

@giordano giordano added the needs pkgeval Tests for all registered packages should be run with this change label Oct 8, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

compiler:codegen Generation of LLVM IR and native code compiler:effects effect analysis needs pkgeval Tests for all registered packages should be run with this change performance Must go faster

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants