Repository navigation
Conversation
fed5486 to
94f0530
Compare
|
The Symptom: Cause: the first revision keyed the memory mapping off Fix: memory claims only when 🤖 Generated with Claude Code |
0b92948 to
d89f4b9
Compare
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)
d89f4b9 to
e7bf730
Compare
|
Seems in principle sound to me, although this stuff is very tricky. Definitely needs a pkgeval. |
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_bitsinto attributes on specsig call sites so LICM, GVN and DSE can use what inference already proved:Out-parameters (
sret,return_roots, and the Union calling convention's result buffer, which carries nosretattribute and is recognized viareturninfo.cc) addargmem: write; the remaining pointer parameters of an:effect_freecallee getreadonly.Example:
A[i] = poly(x)with@noinline polyover 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
"julia.safepoint";LateLowerGCFrame(LateLowerGCFrame: honor the "julia.safepoint" call-site marker #63462) treats tagged calls as safepoints whatever their memory effects and strips the effects before post-lowering passes see the explicit frame stores.readnoneis only emitted for:notaskstatecallees; dynamic dispatch,current_scope,current_taskandccallall clear that bit, so such a callee cannot observe world age or scope through pgcstack.WeakRef.valueloads and finalizer side effects may be reordered across such a call; both are already permitted at the Julia level for:effect_freecallees (per the consensus inadd_fn_attrs_for_effectsmarks calls that can write memory asmemory(read: argmem)#62613).Why
:consistentis 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.doneis(+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
:inaccessiblememonlyalone (so?creaders gotmemory(read)/memory(argmem: read)); it livelockedthreads_exec.jlwithJULIA_NUM_THREADS=2,0on x86_64-linux-gnu — both threads at 100 % intrypoptask → multiq_deletemin, one task stuck inChannelput!, the other intake!— 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_NOTRETURNEDcallees return a fresh mutable object, so two calls must never be merged (GVN/EarlyCSE value-number readonly calls). They get an extrainaccessiblemem: readwrite, which keeps load/store forwarding across the call but makes it neither mergeable nor hoistable.sretattribute, so out-parameters are derived fromreturninfo.cc. (Missing this produced garbage results during bootstrap in an earlier revision.)Safety valve
JULIA_LLVM_ARGS=-julia-effects-attrs=0disables the translation, so a miscompile found in the field can be worked around without a revert. The mapping and its invariants are documented indoc/src/devdocs/llvm.md.Testing
make testalllocally (x86_64-linux, assertions on): all sets green except threePkg/test/extensions.jlcoverage assertions that fail identically on unmodified master on this machine.WITH_GC_VERIFY=1build (assertions on): sysimage + all stdlib pkgimages build clean, andgc core strings threads channels misc(11.3M assertions) plusCompiler/{codegen,effects}pass with no GC verification errors.test/llvmpasseslit suite 56/56, incl. a new end-to-end rooting test (safepoint-effects-rooting.jl).Compiler/test/codegen.jlcases for every row of the table, sret/Union out-params,readonlyparameters, the fresh-allocation no-CSE case, a@noinlineatomic-load spin-wait (call must stay in the loop, no memory claim), LICM hoisting and theBase.blackboxnegative.add_fn_attrs_for_effectsmarks calls that can write memory asmemory(read: argmem)#62613 example and a barrier/store stress underGC.gc(false).:consistentfix:threadstest set 3× green locally.Not yet run on this revision: nanosoldier benchmarks / PkgEval (the in-flight
gh-5884395480run on #63465 covers the previous revision and only compares #63465 against this PR's branch), Windows/i686 CI.Follow-ups this motivates
: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?creaders likelength(v)/r[]getmemory(read)/argmem: readsafely — today:consistentis the only proxy for "no synchronization".noalias-return fact soCONSISTENT_IF_NOTRETURNEDcallees keep the readonly claim without the CSE hazard.🤖 Generated with Claude Code