Skip to content

codegen: Move GC pointer zeroing to late-gc-lowering#60924

Merged
gbaraldi merged 5 commits intomasterfrom
gb/gc-alloc-zeroing-clean
Feb 9, 2026
Merged

codegen: Move GC pointer zeroing to late-gc-lowering#60924
gbaraldi merged 5 commits intomasterfrom
gb/gc-alloc-zeroing-clean

Conversation

@gbaraldi
Copy link
Copy Markdown
Member

@gbaraldi gbaraldi commented Feb 4, 2026

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

   %"new::RefValue" = call noalias nonnull align 8 dereferenceable(16) ptr @ijl_gc_small_alloc(ptr %ptls_load3, i32 360, i32 16, i64 140078842429264) #5, !dbg !25
   %"new::RefValue.tag_addr" = getelementptr inbounds i8, ptr %"new::RefValue", i64 -8, !dbg !25
   store atomic i64 140078842429264, ptr %"new::RefValue.tag_addr" unordered, align 8, !dbg !25, !tbaa !30
   store ptr null, ptr %"new::RefValue", align 8, !dbg !25, !tbaa !33, !alias.scope !36, !noalias !39
   store atomic ptr %"x::Array", ptr %"new::RefValue" release, align 8, !dbg !25, !tbaa !33, !alias.scope !36, !noalias !39
   ret ptr %"new::RefValue", !dbg !25

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 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

gbaraldi and others added 2 commits February 4, 2026 15:49
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>
@Keno
Copy link
Copy Markdown
Member

Keno commented Feb 4, 2026

Design looks sensible to me - didn't look at the implementation, but if it works, I'm happy.

gbaraldi and others added 2 commits February 5, 2026 09:22
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>
@gbaraldi gbaraldi force-pushed the gb/gc-alloc-zeroing-clean branch from a10033f to aa5d58d Compare February 5, 2026 12:44
- 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>
@gbaraldi gbaraldi force-pushed the gb/gc-alloc-zeroing-clean branch from 166a152 to e56b428 Compare February 5, 2026 13:58
@gbaraldi
Copy link
Copy Markdown
Member Author

gbaraldi commented Feb 5, 2026

@nanosoldier runtests()

@nanosoldier
Copy link
Copy Markdown
Collaborator

The package evaluation job you requested has completed - possible new issues were detected.
The full report is available.

Report summary

❗ Packages that crashed

2 packages crashed only on the current version.

  • The process was aborted: 1 packages
  • A segmentation fault happened: 1 packages

272 packages crashed on the previous version too.

✖ Packages that failed

13 packages failed only on the current version.

  • Package has test failures: 2 packages
  • Package tests unexpectedly errored: 1 packages
  • Tests became inactive: 1 packages
  • Test duration exceeded the time limit: 9 packages

1220 packages failed on the previous version too.

✔ Packages that passed tests

14 packages passed tests only on the current version.

  • Other: 14 packages

5549 packages passed tests on the previous version too.

~ Packages that at least loaded

3430 packages successfully loaded on the previous version too.

➖ Packages that were skipped altogether

902 packages were skipped on the previous version too.

@gbaraldi
Copy link
Copy Markdown
Member Author

gbaraldi commented Feb 9, 2026

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

@vchuravy
Copy link
Copy Markdown
Member

vchuravy commented Feb 9, 2026

@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.

@gbaraldi gbaraldi merged commit 4fe5312 into master Feb 9, 2026
9 checks passed
@gbaraldi gbaraldi deleted the gb/gc-alloc-zeroing-clean branch February 9, 2026 18:12
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.

5 participants