codegen: Move GC pointer zeroing to late-gc-lowering#60924
Conversation
Move GC pointer field zeroing from codegen to late-gc-lowering to prevent optimization passes from sinking the null stores past safepoints. This ensures the GC never sees uninitialized pointer values. The implementation uses LLVM operand bundles on the gc_alloc_obj call: - `julia.gc_alloc_ptr_offsets(i64 off1, i64 off2, ...)`: specifies byte offsets of GC pointer fields that need null initialization - `julia.gc_alloc_zeroinit(i64 offset, i64 size)`: specifies a contiguous region to zero (for GenericMemory with boxed elements) Late-gc-lowering processes these bundles after lowering the allocation to gc_alloc_bytes and emits the appropriate null stores or memset calls. Since this happens after optimization passes, the zeroing cannot be incorrectly sunk past safepoints. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Extend the GC pointer zeroing infrastructure to handle variable-length GenericMemory allocations via the new `julia.gc_alloc_zeroinit_indirect` operand bundle. The three bundles are now: - `julia.gc_alloc_ptr_offsets`: null individual pointers at scattered offsets (for structs with mixed pointer/non-pointer fields) - `julia.gc_alloc_zeroinit`: zero contiguous region at fixed offset (for pooled Memory with inline data) - `julia.gc_alloc_zeroinit_indirect`: load data pointer from header offset, zero via that pointer (for variable-length Memory where data location is determined at runtime) Also adds assertions in late-gc-lowering to ensure bundles are used with the correct allocation functions: - ptr_offsets and zeroinit only on julia.gc_alloc_obj - zeroinit_indirect only on jl_alloc_genericmemory_unchecked Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
Design looks sensible to me - didn't look at the implementation, but if it works, I'm happy. |
Move svec GC pointer zeroing to late-gc-lowering using the julia.gc_alloc_zeroinit bundle. Previously, the memset to zero the pointer array was done directly in codegen, which could be sunk by optimization passes past safepoints, leaving the GC to see uninitialized pointers. The svec layout is [length][ptr0][ptr1]...[ptr_n-1], so the zeroinit region starts at offset sizeof_ptr and has size sizeof_ptr * nargs. Also add test for svec allocation zeroing and fix the late-gc-lowering tests to match actual LLVM attribute output for jl_alloc_genericmemory_unchecked. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The GC zeroing changes enable better escape analysis for Memory allocations with Union element types. The allocation can now be fully elided, so this test is no longer broken. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
a10033f to
aa5d58d
Compare
- Use offsetof(jl_genericmemory_t, ptr) instead of hardcoded sizeof(void*) - Use sizeof(jl_svec_t) for svec pointer array offset - Add comment in jl_alloc_genericmemory_unchecked to keep offset in sync with emit_const_len_memorynew - Add assertion that ptr_offsets and zeroinit bundles are mutually exclusive - Change bundle size checks from if-statements to assertions Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
166a152 to
e56b428
Compare
|
@nanosoldier |
|
The package evaluation job you requested has completed - possible new issues were detected. Report summary❗ Packages that crashed2 packages crashed only on the current version.
272 packages crashed on the previous version too. ✖ Packages that failed13 packages failed only on the current version.
1220 packages failed on the previous version too. ✔ Packages that passed tests14 packages passed tests only on the current version.
5549 packages passed tests on the previous version too. ~ Packages that at least loaded3430 packages successfully loaded on the previous version too. ➖ Packages that were skipped altogether902 packages were skipped on the previous version too. |
|
Pkgeval looked good. I couldn't repro the xml failure and it happened deep inside libxml so hopefully not related. The other is enzyme related which is expected |
|
@wsmoses this ought to simplify tape emission a bit since we don't have to insert the null stores, but we do need to emit the right bundle. |
This still needs some cleanup, but after a lot of discussion and some experimentation it dawned on me that if we always initialize after late gc lowering runs then there's no need for tricks to hide this from LLVM.
There are some caveats (that exist today so this is not a regression). Codegen currently always emits stores of GC tracked fields as release atomics, but doesn't do that for any of it's null initialization. For reasons of LLVM never touching atomics, that stops all optimizations of the null pointer stores.
This causes things like
for something like
@code_llvm raw=true dump_module=true Ref([])AI PART
Move GC pointer field zeroing from codegen to late-gc-lowering to prevent
optimization passes from sinking the null stores past safepoints. This
ensures the GC never sees uninitialized pointer values.
The implementation uses LLVM operand bundles on the gc_alloc_obj call:
julia.gc_alloc_ptr_offsets(i64 off1, i64 off2, ...): specifies byteoffsets of GC pointer fields that need null initialization
julia.gc_alloc_zeroinit(i64 offset, i64 size): specifies a contiguousregion to zero (for GenericMemory with boxed elements)
Late-gc-lowering processes these bundles after lowering the allocation
to gc_alloc_bytes and emits the appropriate null stores or memset calls.
Since this happens after optimization passes, the zeroing cannot be
incorrectly sunk past safepoints.
Co-Authored-By: Claude Opus 4.5 noreply@anthropic.com