v2 / vlib / builtin / vgc_d_vgc.c.v
996 lines · 913 sloc · 28.28 KB · e2e5cf8db56f3562c7baa735061690be936bdf3e
Raw
1// vgc_d_vgc.c.v - V Garbage Collector: Core types, heap, and allocation
2// Translated from Go's runtime GC (golang/go src/runtime/malloc.go, mheap.go, mspan.go, mcache.go, mcentral.go)
3// Concurrent tri-color mark-and-sweep garbage collector with size-class allocation.
4
5@[has_globals]
6module builtin
7
8#flag -I @VEXEROOT/thirdparty/vgc
9#include "vgc_platform.h"
10
11// C interop declarations for platform header
12fn C.vgc_get_cache_idx() int
13fn C.vgc_set_cache_idx(idx int)
14fn C.vgc_atomic_load_u32(ptr &u32) u32
15fn C.vgc_atomic_store_u32(ptr &u32, val u32)
16fn C.vgc_atomic_load_u64(ptr &u64) u64
17fn C.vgc_atomic_store_u64(ptr &u64, val u64)
18fn C.vgc_atomic_add_u64(ptr &u64, val u64) u64
19fn C.vgc_atomic_sub_u64(ptr &u64, val u64) u64
20fn C.vgc_atomic_add_u32(ptr &u32, val u32) u32
21fn C.vgc_atomic_cas_u32(ptr &u32, expected &u32, desired u32) bool
22fn C.vgc_atomic_exchange_u32(ptr &u32, val u32) u32
23fn C.vgc_atomic_fence()
24fn C.vgc_os_alloc(size usize) voidptr
25fn C.vgc_os_free(ptr voidptr, size usize)
26fn C.vgc_os_decommit(ptr voidptr, size usize)
27fn C.vgc_get_sp() voidptr
28fn C.vgc_get_stack_bounds(lo &usize, hi &usize) int
29fn C.vgc_bitmap_get(bits &u8, idx u32) int
30fn C.vgc_bitmap_set(bits &u8, idx u32)
31fn C.vgc_bitmap_clear(bits &u8, idx u32)
32fn C.vgc_bitmap_test_and_set(bits &u8, idx u32) int
33fn C.vgc_popcount8(x u8) int
34fn C.vgc_size_class(size u32) u8
35fn C.vgc_get_class_size(cls int) u32
36fn C.vgc_get_class_npages(cls int) u32
37fn C.vgc_get_class_nobjs(cls int) u32
38fn C.vgc_init_size_tables()
39fn C.vgc_mutex_lock(lk &u32)
40fn C.vgc_mutex_unlock(lk &u32)
41fn C.vgc_start_thread(f voidptr)
42fn C.vgc_num_cpus() int
43fn C.vgc_addr_map_register(base usize, size usize, arena_idx int)
44fn C.vgc_addr_to_arena(addr usize) int
45
46// ============================================================
47// Constants (translated from Go's runtime constants)
48// ============================================================
49
50const vgc_page_shift = 13
51const vgc_page_size = 8192
52const vgc_max_small_size = u32(32768) // objects > this are "large"
53const vgc_num_classes = 68
54const vgc_num_span_classes = 136 // 68 * 2 (scan + noscan variants)
55const vgc_arena_size = usize(64) * 1024 * 1024 // 64MB per arena
56const vgc_pages_per_arena = vgc_arena_size / vgc_page_size
57const vgc_max_arenas = 64
58const vgc_max_threads = 64
59const vgc_tiny_size = 16 // tiny allocator threshold (no-pointer objects < 16 bytes)
60
61// GC phases (translated from Go's _GCoff, _GCmark, _GCmarktermination)
62const vgc_phase_off = u32(0)
63const vgc_phase_mark = u32(1)
64const vgc_phase_mark_term = u32(2)
65const vgc_phase_sweep = u32(3)
66
67// ============================================================
68// Core types (translated from Go's mspan, mheap, mcache, mcentral)
69// ============================================================
70
71// VGC_Span represents a run of contiguous pages containing objects of one size class.
72// Translated from Go's runtime.mspan.
73struct VGC_Span {
74mut:
75 base usize // start address of the span
76 npages u32 // number of pages in this span
77 elem_size u32 // size of each object in bytes
78 nelems u32 // number of elements (objects) in the span
79 class_idx u8 // size class index (0 for large objects)
80 noscan bool // true if objects contain no pointers (noscan variant)
81 in_use bool // true if span is allocated to a size class
82 has_ptrmap bool // true if ptrmap is valid (precise scanning available)
83 // Pointer bitmap: bit N = word offset N contains a pointer.
84 // Covers objects up to 512 bytes (64 words on 64-bit).
85 // For larger objects, falls back to conservative scanning.
86 ptrmap u64
87 // Number of pointer words in the object (for precise scanning)
88 ptr_words u8
89 // Allocation bitmaps (translated from Go's allocBits/gcmarkBits)
90 alloc_bits &u8 = unsafe { nil } // 1 = allocated
91 mark_bits &u8 = unsafe { nil } // 1 = marked (used during GC)
92 alloc_count u32 // number of currently allocated objects
93 free_index u32 // hint: scan from here for free slot
94 // Sweep generation (translated from Go's sweepgen)
95 sweep_gen u32
96 // Linked list pointers for central free lists
97 next &VGC_Span = unsafe { nil }
98 prev &VGC_Span = unsafe { nil }
99}
100
101// VGC_Cache is a per-thread allocation cache.
102// Translated from Go's runtime.mcache - eliminates lock contention on hot path.
103struct VGC_Cache {
104mut:
105 alloc [136]&VGC_Span // one span per span class (68 scan + 68 noscan)
106 // Tiny allocator for objects < 16 bytes without pointers
107 // Translated from Go's mcache.tiny
108 tiny usize
109 tiny_offset usize
110 tiny_allocs usize
111 // Thread info
112 registered bool
113 stack_base usize // fixed stack boundary for this thread
114 stack_lo usize // lowest stack address (for root scanning)
115 stack_hi usize // highest stack address
116 thread_id u64
117 stopped u32 // 1 if stopped for GC
118}
119
120// VGC_Central is a central free list for one span class.
121// Translated from Go's runtime.mcentral.
122struct VGC_Central {
123mut:
124 lock u32 // spinlock
125 partial &VGC_Span = unsafe { nil } // spans with free objects (swept)
126 full &VGC_Span = unsafe { nil } // spans with no free objects
127}
128
129// VGC_Arena tracks a chunk of memory obtained from the OS.
130// Translated from Go's heapArena concept.
131struct VGC_Arena {
132mut:
133 base usize
134 size usize
135 used usize
136 // Map from page index to span (for finding which span owns an address)
137 page_span [8192]&VGC_Span // vgc_pages_per_arena entries
138}
139
140// VGC_WorkBuf is a work buffer for the mark phase.
141// Translated from Go's runtime.workbuf.
142struct VGC_WorkBuf {
143mut:
144 nobj int
145 obj [256]usize // pointers to mark
146 next &VGC_WorkBuf = unsafe { nil }
147}
148
149// VGC_Heap is the global heap.
150// Translated from Go's runtime.mheap.
151struct VGC_Heap {
152mut:
153 lock u32 // spinlock
154 // Arenas (memory from OS)
155 arenas [64]VGC_Arena
156 narenas int
157 // Central free lists (one per span class)
158 central [136]VGC_Central
159 // Large object spans
160 large_alloc &VGC_Span = unsafe { nil }
161 // All spans for iteration during GC
162 allspans [16384]&VGC_Span
163 nspans int
164 // Free spans (completely empty, reusable by page count)
165 free_spans_lock u32
166 free_spans [32]&VGC_Span // free spans indexed by npages (1..31, 0=unused)
167 // Per-thread caches
168 caches [64]VGC_Cache
169 ncaches int
170 cache_lock u32
171 // GC state
172 gc_phase u32 // atomic: current GC phase
173 gc_enabled u32 // atomic: 1 = GC enabled
174 sweep_gen u32 // current sweep generation
175 wb_enabled u32 // atomic: write barrier enabled
176 // GC metrics (translated from Go's gcController)
177 heap_live u64 // atomic: bytes of live heap objects (actual object bytes)
178 heap_marked u64 // bytes marked in last cycle
179 next_gc u64 // trigger next GC at this heap size
180 total_alloc u64 // atomic: total bytes allocated
181 gc_cycle u64 // number of completed GC cycles
182 // GC work queues
183 work_full &VGC_WorkBuf = unsafe { nil }
184 work_empty &VGC_WorkBuf = unsafe { nil }
185 work_lock u32
186 // GC worker coordination
187 gc_workers_done u32 // atomic
188 gc_nworkers int
189 gc_stop_flag u32 // atomic: tells threads to stop for GC
190 gc_stopped_count u32 // atomic: threads stopped
191 gc_target_stops u32 // number of threads to stop
192 // Sweep state
193 sweep_idx int
194 sweep_done u32 // atomic
195 // Default GC trigger: collect when heap doubles (GOGC=100 equivalent)
196 gc_percent int // like Go's GOGC, default 100
197}
198
199// Global heap instance
200__global vgc_heap = VGC_Heap{}
201// Fast bounds check for pointer validation
202__global vgc_arena_lo = usize(0)
203__global vgc_arena_hi = usize(0)
204
205// ============================================================
206// Initialization
207// ============================================================
208
209@[markused]
210pub fn vgc_init() {
211 C.vgc_init_size_tables()
212 vgc_heap.gc_enabled = 1
213 vgc_heap.gc_percent = 100
214 vgc_heap.next_gc = 256 * 1024 * 1024 // favor throughput over early collections
215 vgc_heap.gc_phase = vgc_phase_off
216 // Register the main thread
217 vgc_register_thread()
218}
219
220// ============================================================
221// Thread registration (for root scanning)
222// ============================================================
223
224fn vgc_register_thread() {
225 C.vgc_mutex_lock(&vgc_heap.cache_lock)
226 idx := vgc_heap.ncaches
227 if idx >= vgc_max_threads {
228 C.vgc_mutex_unlock(&vgc_heap.cache_lock)
229 return
230 }
231 vgc_heap.ncaches = idx + 1
232 C.vgc_mutex_unlock(&vgc_heap.cache_lock)
233
234 C.vgc_set_cache_idx(idx)
235 sp := usize(C.vgc_get_sp())
236 mut stack_lo := usize(0)
237 mut stack_hi := usize(0)
238 mut stack_base := if sp > usize(8) * 1024 * 1024 {
239 sp - usize(8) * 1024 * 1024
240 } else {
241 usize(0)
242 }
243 if C.vgc_get_stack_bounds(&stack_lo, &stack_hi) != 0 && stack_lo < stack_hi {
244 dist_lo := if sp >= stack_lo { sp - stack_lo } else { stack_lo - sp }
245 dist_hi := if stack_hi >= sp { stack_hi - sp } else { sp - stack_hi }
246 stack_base = if dist_hi <= dist_lo { stack_hi } else { stack_lo }
247 }
248 unsafe {
249 vgc_heap.caches[idx].registered = true
250 vgc_heap.caches[idx].stack_base = stack_base
251 }
252 vgc_refresh_stack_range_for_sp(idx, sp)
253}
254
255fn vgc_ensure_registered() {
256 if C.vgc_get_cache_idx() < 0 {
257 vgc_register_thread()
258 }
259}
260
261fn vgc_refresh_stack_range() {
262 cache_idx := C.vgc_get_cache_idx()
263 if cache_idx < 0 {
264 return
265 }
266 vgc_refresh_stack_range_for_sp(cache_idx, usize(C.vgc_get_sp()))
267}
268
269fn vgc_refresh_stack_range_for_sp(cache_idx int, sp usize) {
270 if cache_idx < 0 || cache_idx >= vgc_max_threads {
271 return
272 }
273 stack_base := unsafe { vgc_heap.caches[cache_idx].stack_base }
274 if stack_base <= sp {
275 unsafe {
276 vgc_heap.caches[cache_idx].stack_lo = stack_base
277 vgc_heap.caches[cache_idx].stack_hi = sp
278 }
279 } else {
280 unsafe {
281 vgc_heap.caches[cache_idx].stack_lo = sp
282 vgc_heap.caches[cache_idx].stack_hi = stack_base
283 }
284 }
285}
286
287// ============================================================
288// Span management (translated from Go's mspan operations)
289// ============================================================
290
291// Try to get a recycled span from the free list
292fn vgc_get_free_span(npages u32) &VGC_Span {
293 if npages == 0 || npages >= 32 {
294 return unsafe { nil }
295 }
296 C.vgc_mutex_lock(&vgc_heap.free_spans_lock)
297 span := vgc_heap.free_spans[npages]
298 if span != unsafe { nil } {
299 unsafe {
300 vgc_heap.free_spans[npages] = span.next
301 span.next = nil
302 span.prev = nil
303 span.in_use = true
304 }
305 C.vgc_mutex_unlock(&vgc_heap.free_spans_lock)
306 return span
307 }
308 C.vgc_mutex_unlock(&vgc_heap.free_spans_lock)
309 return unsafe { nil }
310}
311
312// Return a fully-empty span to the free list for reuse
313fn vgc_put_free_span(mut span VGC_Span) {
314 npages := span.npages
315 if npages == 0 || npages >= 32 {
316 return
317 }
318 // Free bitmaps before zeroing nelems
319 bitmap_size := usize((span.nelems + 7) / 8)
320 if span.alloc_bits != unsafe { nil } {
321 C.vgc_os_free(span.alloc_bits, bitmap_size)
322 span.alloc_bits = unsafe { nil }
323 }
324 if span.mark_bits != unsafe { nil } {
325 C.vgc_os_free(span.mark_bits, bitmap_size)
326 span.mark_bits = unsafe { nil }
327 }
328 span.in_use = false
329 span.class_idx = 0
330 span.elem_size = 0
331 span.nelems = 0
332 span.alloc_count = 0
333 span.free_index = 0
334 // Decommit pages to return physical memory to OS
335 page_bytes := usize(npages) * vgc_page_size
336 C.vgc_os_decommit(voidptr(span.base), page_bytes)
337 C.vgc_mutex_lock(&vgc_heap.free_spans_lock)
338 unsafe {
339 span.next = vgc_heap.free_spans[npages]
340 vgc_heap.free_spans[npages] = span
341 }
342 C.vgc_mutex_unlock(&vgc_heap.free_spans_lock)
343}
344
345// Allocate a new span with the given number of pages
346fn vgc_span_alloc(npages u32) &VGC_Span {
347 // First try to reuse a free span
348 recycled := vgc_get_free_span(npages)
349 if recycled != unsafe { nil } {
350 return recycled
351 }
352
353 nbytes := usize(npages) * vgc_page_size
354
355 C.vgc_mutex_lock(&vgc_heap.lock)
356 // Try to find space in existing arenas
357 mut base := usize(0)
358 mut arena_idx := -1
359 for i in 0 .. vgc_heap.narenas {
360 a := unsafe { &vgc_heap.arenas[i] }
361 if a.used + nbytes <= a.size {
362 base = a.base + a.used
363 arena_idx = i
364 unsafe {
365 vgc_heap.arenas[i].used += nbytes
366 }
367 break
368 }
369 }
370 // Allocate new arena if needed
371 if base == 0 {
372 asize := if nbytes > vgc_arena_size { nbytes } else { vgc_arena_size }
373 mem := C.vgc_os_alloc(asize)
374 if mem == unsafe { nil } {
375 C.vgc_mutex_unlock(&vgc_heap.lock)
376 return unsafe { nil }
377 }
378 arena_idx = vgc_heap.narenas
379 if arena_idx >= vgc_max_arenas {
380 C.vgc_os_free(mem, asize)
381 C.vgc_mutex_unlock(&vgc_heap.lock)
382 return unsafe { nil }
383 }
384 unsafe {
385 vgc_heap.arenas[arena_idx].base = usize(mem)
386 vgc_heap.arenas[arena_idx].size = asize
387 vgc_heap.arenas[arena_idx].used = nbytes
388 }
389 vgc_heap.narenas = arena_idx + 1
390 base = usize(mem)
391 // Register in address map for O(1) lookup
392 C.vgc_addr_map_register(usize(mem), asize, arena_idx)
393 // Update global arena bounds for fast pointer rejection
394 if vgc_arena_lo == 0 || base < vgc_arena_lo {
395 vgc_arena_lo = base
396 }
397 arena_end := base + asize
398 if arena_end > vgc_arena_hi {
399 vgc_arena_hi = arena_end
400 }
401 }
402
403 // Create span metadata (allocate from OS for metadata to avoid chicken-and-egg)
404 span := unsafe { &VGC_Span(C.vgc_os_alloc(sizeof(VGC_Span))) }
405 if span == unsafe { nil } {
406 C.vgc_mutex_unlock(&vgc_heap.lock)
407 return unsafe { nil }
408 }
409 unsafe {
410 C.memset(span, 0, sizeof(VGC_Span))
411 span.base = base
412 span.npages = npages
413 span.in_use = true
414 }
415 // Register span in arena's page map
416 if arena_idx >= 0 {
417 page_start := (base - vgc_heap.arenas[arena_idx].base) / vgc_page_size
418 for p in 0 .. npages {
419 pidx := page_start + p
420 if pidx < vgc_pages_per_arena {
421 unsafe {
422 vgc_heap.arenas[arena_idx].page_span[pidx] = span
423 }
424 }
425 }
426 }
427
428 // Track in allspans
429 if vgc_heap.nspans < 16384 {
430 unsafe {
431 vgc_heap.allspans[vgc_heap.nspans] = span
432 }
433 vgc_heap.nspans++
434 }
435
436 C.vgc_mutex_unlock(&vgc_heap.lock)
437 return span
438}
439
440// Initialize a span for a specific size class
441fn vgc_span_init(mut span VGC_Span, class_idx u8, noscan bool) {
442 size := C.vgc_get_class_size(int(class_idx))
443 npages := C.vgc_get_class_npages(int(class_idx))
444 nobjs := C.vgc_get_class_nobjs(int(class_idx))
445
446 span.class_idx = class_idx
447 span.noscan = noscan
448 span.elem_size = size
449 span.npages = npages
450 span.nelems = nobjs
451 span.free_index = 0
452 span.alloc_count = 0
453
454 // Allocate bitmaps: ceil(nobjs/8) bytes each
455 bitmap_size := (nobjs + 7) / 8
456 span.alloc_bits = unsafe { &u8(C.vgc_os_alloc(usize(bitmap_size))) }
457 span.mark_bits = unsafe { &u8(C.vgc_os_alloc(usize(bitmap_size))) }
458 if span.alloc_bits != unsafe { nil } {
459 unsafe { C.memset(span.alloc_bits, 0, bitmap_size) }
460 }
461 if span.mark_bits != unsafe { nil } {
462 unsafe { C.memset(span.mark_bits, 0, bitmap_size) }
463 }
464}
465
466// Find a free slot in a span and allocate it
467fn vgc_span_alloc_obj(mut span VGC_Span) voidptr {
468 if span.alloc_bits == unsafe { nil } {
469 return unsafe { nil }
470 }
471 start_idx := span.free_index
472 nbytes := (span.nelems + 7) >> 3
473 start_byte := start_idx >> 3
474 end_byte := (start_idx + 7) >> 3
475 for pass in 0 .. 2 {
476 mut byte_idx := if pass == 0 { start_byte } else { u32(0) }
477 limit := if pass == 0 { nbytes } else { end_byte }
478 for byte_idx < limit {
479 bit_base := byte_idx << 3
480 mut b := unsafe { span.alloc_bits[byte_idx] }
481 if b == 0xFF {
482 byte_idx++
483 continue
484 }
485 start_bit := if byte_idx == start_byte { start_idx & 7 } else { u32(0) }
486 for bit := start_bit; bit < u32(8); bit++ {
487 i := bit_base + bit
488 if i >= span.nelems {
489 break
490 }
491 mask := u8(1) << bit
492 if (b & mask) == 0 {
493 b |= mask
494 unsafe {
495 span.alloc_bits[byte_idx] = b
496 }
497 span.alloc_count++
498 span.free_index = i + 1
499 addr := span.base + usize(i) * usize(span.elem_size)
500 return unsafe { voidptr(addr) }
501 }
502 }
503 byte_idx++
504 }
505 }
506 return unsafe { nil } // span is full
507}
508
509// ============================================================
510// Central free list operations (translated from Go's mcentral)
511// ============================================================
512
513// Get a span with free objects for the given span class
514fn vgc_central_get_span(span_class int) &VGC_Span {
515 central := unsafe { &vgc_heap.central[span_class] }
516 C.vgc_mutex_lock(¢ral.lock)
517
518 // Try partial list first (spans with free objects)
519 mut span := central.partial
520 if span != unsafe { nil } {
521 // Remove from partial list
522 unsafe {
523 vgc_heap.central[span_class].partial = span.next
524 }
525 if span.next != unsafe { nil } {
526 unsafe {
527 span.next.prev = nil
528 }
529 }
530 unsafe {
531 span.next = nil
532 span.prev = nil
533 }
534 C.vgc_mutex_unlock(¢ral.lock)
535 return span
536 }
537
538 C.vgc_mutex_unlock(¢ral.lock)
539
540 // No spans available - allocate a new one
541 class_idx := u8(span_class / 2)
542 noscan := (span_class % 2) == 1
543 npages := C.vgc_get_class_npages(int(class_idx))
544 new_span := vgc_span_alloc(npages)
545 if new_span == unsafe { nil } {
546 return unsafe { nil }
547 }
548 unsafe {
549 vgc_span_init(mut new_span, class_idx, noscan)
550 }
551
552 return new_span
553}
554
555// Return a span to the central free list
556fn vgc_central_return_span(span_class int, mut span VGC_Span) {
557 central := unsafe { &vgc_heap.central[span_class] }
558 C.vgc_mutex_lock(¢ral.lock)
559
560 if span.alloc_count < span.nelems {
561 // Has free objects - add to partial
562 unsafe {
563 span.next = vgc_heap.central[span_class].partial
564 span.prev = nil
565 }
566 if span.next != unsafe { nil } {
567 unsafe {
568 span.next.prev = span
569 }
570 }
571 unsafe {
572 vgc_heap.central[span_class].partial = span
573 }
574 } else {
575 // Full - add to full list
576 unsafe {
577 span.next = vgc_heap.central[span_class].full
578 span.prev = nil
579 }
580 if span.next != unsafe { nil } {
581 unsafe {
582 span.next.prev = span
583 }
584 }
585 unsafe {
586 vgc_heap.central[span_class].full = span
587 }
588 }
589
590 C.vgc_mutex_unlock(¢ral.lock)
591}
592
593// ============================================================
594// Cache operations (translated from Go's mcache)
595// ============================================================
596
597fn vgc_cache_get_span(cache_idx int, span_class int) &VGC_Span {
598 span := unsafe { vgc_heap.caches[cache_idx].alloc[span_class] }
599 if span != unsafe { nil } {
600 // Check if span has free objects
601 if span.alloc_count < span.nelems {
602 return span
603 }
604 // Span is full - return to central and get a new one
605 unsafe {
606 vgc_central_return_span(span_class, mut span)
607 }
608 }
609 // Get fresh span from central
610 new_span := vgc_central_get_span(span_class)
611 unsafe {
612 vgc_heap.caches[cache_idx].alloc[span_class] = new_span
613 }
614 return new_span
615}
616
617// ============================================================
618// Main allocation entry points
619// (translated from Go's runtime.mallocgc)
620// ============================================================
621
622fn vgc_malloc(n usize) voidptr {
623 return vgc_malloc_typed_opts(n, 0, 0, true)
624}
625
626// vgc_malloc_typed allocates with a precise pointer map.
627// ptrmap: bitmap where bit N means word offset N is a pointer.
628// ptr_words: number of pointer words in the object.
629// If ptrmap==0 && ptr_words==0, falls back to conservative scanning.
630fn vgc_malloc_typed(n usize, ptrmap u64, ptr_words u8) voidptr {
631 return vgc_malloc_typed_opts(n, ptrmap, ptr_words, true)
632}
633
634fn vgc_malloc_typed_opts(n usize, ptrmap u64, ptr_words u8, zero_fill bool) voidptr {
635 if n == 0 {
636 return unsafe { nil }
637 }
638
639 vgc_ensure_registered()
640 cache_idx := C.vgc_get_cache_idx()
641
642 // Large allocation (> 32KB) - get dedicated span
643 if n > usize(vgc_max_small_size) {
644 vgc_maybe_gc()
645 return vgc_alloc_large(n, false, zero_fill)
646 }
647
648 // Small allocation - use size class and cache
649 class_idx := C.vgc_size_class(u32(n))
650 if class_idx == 0 {
651 vgc_maybe_gc()
652 return vgc_alloc_large(n, false, zero_fill)
653 }
654
655 span_class := int(class_idx) * 2 // scan variant
656 span := vgc_cache_get_span(cache_idx, span_class)
657 if span == unsafe { nil } {
658 return unsafe { nil }
659 }
660
661 // Set precise pointer map on span (first typed allocation wins)
662 if ptrmap != 0 && !span.has_ptrmap {
663 unsafe {
664 (&VGC_Span(span)).has_ptrmap = true
665 (&VGC_Span(span)).ptrmap = ptrmap
666 (&VGC_Span(span)).ptr_words = ptr_words
667 }
668 }
669
670 ptr := unsafe { vgc_span_alloc_obj(mut span) }
671 if ptr != unsafe { nil } {
672 // Track actual object bytes, not page bytes
673 C.vgc_atomic_add_u64(&vgc_heap.heap_live, u64(span.elem_size))
674 C.vgc_atomic_add_u64(&vgc_heap.total_alloc, u64(n))
675 if zero_fill {
676 unsafe { C.memset(ptr, 0, n) }
677 }
678 // Periodic GC check - only when span fills up (amortize cost)
679 if span.alloc_count >= span.nelems {
680 vgc_maybe_gc()
681 }
682 }
683 return ptr
684}
685
686// Amortized GC trigger check - avoids atomic loads on every allocation
687fn vgc_maybe_gc() {
688 if C.vgc_atomic_load_u32(&vgc_heap.gc_enabled) != 0 {
689 heap_live := C.vgc_atomic_load_u64(&vgc_heap.heap_live)
690 next_gc := C.vgc_atomic_load_u64(&vgc_heap.next_gc)
691 if heap_live >= next_gc {
692 vgc_gc_start()
693 }
694 if C.vgc_atomic_load_u32(&vgc_heap.gc_stop_flag) != 0 {
695 vgc_safepoint()
696 }
697 }
698}
699
700fn vgc_malloc_noscan(n usize) voidptr {
701 return vgc_malloc_noscan_opts(n, true)
702}
703
704fn vgc_malloc_noscan_opts(n usize, zero_fill bool) voidptr {
705 if n == 0 {
706 return unsafe { nil }
707 }
708
709 vgc_ensure_registered()
710 cache_idx := C.vgc_get_cache_idx()
711
712 if n > usize(vgc_max_small_size) {
713 vgc_maybe_gc()
714 return vgc_alloc_large(n, true, zero_fill)
715 }
716
717 class_idx := C.vgc_size_class(u32(n))
718 if class_idx == 0 {
719 return vgc_alloc_large(n, true, zero_fill)
720 }
721
722 // Tiny allocator for very small objects (translated from Go's mcache tiny allocator)
723 if n < vgc_tiny_size && cache_idx >= 0 {
724 cache := unsafe { &vgc_heap.caches[cache_idx] }
725 if cache.tiny != 0 {
726 // Align up for the allocation
727 mut off := cache.tiny_offset
728 if n >= 8 {
729 off = (off + 7) & ~usize(7)
730 } else if n >= 4 {
731 off = (off + 3) & ~usize(3)
732 } else if n >= 2 {
733 off = (off + 1) & ~usize(1)
734 }
735 if off + n <= vgc_tiny_size {
736 ptr := unsafe { voidptr(cache.tiny + off) }
737 unsafe {
738 vgc_heap.caches[cache_idx].tiny_offset = off + n
739 vgc_heap.caches[cache_idx].tiny_allocs++
740 }
741 C.vgc_atomic_add_u64(&vgc_heap.total_alloc, u64(n))
742 return ptr
743 }
744 }
745 // Allocate a new tiny block
746 span_class := int(class_idx) * 2 + 1 // noscan
747 span := vgc_cache_get_span(cache_idx, span_class)
748 if span != unsafe { nil } {
749 ptr := unsafe { vgc_span_alloc_obj(mut span) }
750 if ptr != unsafe { nil } {
751 if zero_fill {
752 unsafe { C.memset(ptr, 0, usize(span.elem_size)) }
753 }
754 unsafe {
755 vgc_heap.caches[cache_idx].tiny = usize(ptr)
756 vgc_heap.caches[cache_idx].tiny_offset = n
757 vgc_heap.caches[cache_idx].tiny_allocs++
758 }
759 C.vgc_atomic_add_u64(&vgc_heap.heap_live, u64(span.elem_size))
760 C.vgc_atomic_add_u64(&vgc_heap.total_alloc, u64(n))
761 return ptr
762 }
763 }
764 }
765
766 span_class := int(class_idx) * 2 + 1 // noscan variant
767 span := vgc_cache_get_span(cache_idx, span_class)
768 if span == unsafe { nil } {
769 return unsafe { nil }
770 }
771
772 ptr := unsafe { vgc_span_alloc_obj(mut span) }
773 if ptr != unsafe { nil } {
774 C.vgc_atomic_add_u64(&vgc_heap.heap_live, u64(span.elem_size))
775 C.vgc_atomic_add_u64(&vgc_heap.total_alloc, u64(n))
776 if zero_fill {
777 unsafe { C.memset(ptr, 0, n) }
778 }
779 }
780 return ptr
781}
782
783// Allocate a large object (> 32KB) with its own span
784fn vgc_alloc_large(n usize, noscan bool, zero_fill bool) voidptr {
785 npages := u32((n + vgc_page_size - 1) / vgc_page_size)
786 span := vgc_span_alloc(npages)
787 if span == unsafe { nil } {
788 return unsafe { nil }
789 }
790
791 unsafe {
792 span.class_idx = 0
793 span.noscan = noscan
794 span.elem_size = u32(n)
795 span.nelems = 1
796 span.alloc_count = 1
797
798 // Single-element bitmap
799 span.alloc_bits = &u8(C.vgc_os_alloc(1))
800 span.mark_bits = &u8(C.vgc_os_alloc(1))
801 if span.alloc_bits != nil {
802 span.alloc_bits[0] = 1
803 }
804 if span.mark_bits != nil {
805 span.mark_bits[0] = 0
806 }
807 }
808 // Add to large allocation list
809 C.vgc_mutex_lock(&vgc_heap.lock)
810 unsafe {
811 span.next = vgc_heap.large_alloc
812 vgc_heap.large_alloc = span
813 }
814 C.vgc_mutex_unlock(&vgc_heap.lock)
815
816 C.vgc_atomic_add_u64(&vgc_heap.heap_live, u64(n))
817 C.vgc_atomic_add_u64(&vgc_heap.total_alloc, u64(n))
818
819 ptr := unsafe { voidptr(span.base) }
820 if zero_fill {
821 unsafe { C.memset(ptr, 0, n) }
822 }
823 return ptr
824}
825
826// Realloc for VGC-managed memory
827fn vgc_realloc(old_ptr voidptr, new_size usize) voidptr {
828 if old_ptr == unsafe { nil } {
829 return vgc_malloc(new_size)
830 }
831 if new_size == 0 {
832 return unsafe { nil }
833 }
834 // Find the span owning this pointer to get old size
835 old_span := vgc_find_span(old_ptr)
836 if old_span == unsafe { nil } {
837 // Unknown object - just malloc new
838 return vgc_malloc(new_size)
839 }
840 old_size := usize(old_span.elem_size)
841 if new_size <= old_size {
842 return old_ptr // fits in current allocation
843 }
844 // Preserve the original scan policy so raw buffers do not become scan objects.
845 mut new_ptr := unsafe { nil }
846 if old_span.noscan {
847 new_ptr = vgc_malloc_noscan_opts(new_size, false)
848 } else if old_span.has_ptrmap {
849 new_ptr = vgc_malloc_typed_opts(new_size, old_span.ptrmap, old_span.ptr_words, false)
850 } else {
851 new_ptr = vgc_malloc_typed_opts(new_size, 0, 0, false)
852 }
853 if new_ptr != unsafe { nil } {
854 copy_size := if old_size < new_size { old_size } else { new_size }
855 unsafe { C.memcpy(new_ptr, old_ptr, copy_size) }
856 }
857 return new_ptr
858}
859
860// Free is mostly a no-op for GC, but can hint at deallocation
861fn vgc_free(ptr voidptr) {
862 if ptr == unsafe { nil } {
863 return
864 }
865 // In a GC environment, explicit free is optional.
866 // The object will be collected if unreachable.
867 // However, we can mark it as free immediately for reuse.
868 span := vgc_find_span(ptr)
869 if span == unsafe { nil } {
870 return
871 }
872 if span.elem_size == 0 {
873 return
874 }
875 obj_idx := u32((usize(ptr) - span.base) / usize(span.elem_size))
876 if obj_idx < span.nelems && span.alloc_bits != unsafe { nil } {
877 if C.vgc_bitmap_get(span.alloc_bits, obj_idx) != 0 {
878 C.vgc_bitmap_clear(span.alloc_bits, obj_idx)
879 unsafe {
880 span.alloc_count--
881 if obj_idx < span.free_index {
882 span.free_index = obj_idx
883 }
884 }
885 C.vgc_atomic_sub_u64(&vgc_heap.heap_live, u64(span.elem_size))
886 }
887 }
888}
889
890// Calloc (zero-initialized allocation)
891fn vgc_calloc(n usize) voidptr {
892 return vgc_malloc(n) // vgc_malloc already zeroes memory
893}
894
895// Typed memdup: allocate with pointer map and copy source data.
896// Used by HEAP_vgc() macro for struct allocations with known layout.
897@[markused]
898fn vgc_memdup_typed(src voidptr, n isize, ptrmap u64, ptr_words u8) voidptr {
899 if src == unsafe { nil } || n <= 0 {
900 return unsafe { nil }
901 }
902 mem := vgc_malloc_typed_opts(usize(n), ptrmap, ptr_words, false)
903 if mem != unsafe { nil } {
904 unsafe { C.memcpy(mem, src, n) }
905 }
906 return mem
907}
908
909// Memdup variants that skip zero-fill when the destination will be overwritten.
910@[markused]
911fn vgc_memdup(src voidptr, n isize) voidptr {
912 if src == unsafe { nil } || n <= 0 {
913 return unsafe { nil }
914 }
915 mem := vgc_malloc_typed_opts(usize(n), 0, 0, false)
916 if mem != unsafe { nil } {
917 unsafe { C.memcpy(mem, src, n) }
918 }
919 return mem
920}
921
922@[markused]
923fn vgc_memdup_noscan(src voidptr, n isize) voidptr {
924 if src == unsafe { nil } || n <= 0 {
925 return unsafe { nil }
926 }
927 mem := vgc_malloc_noscan_opts(usize(n), false)
928 if mem != unsafe { nil } {
929 unsafe { C.memcpy(mem, src, n) }
930 }
931 return mem
932}
933
934// ============================================================
935// Span lookup (find which span owns an address) - O(1) via address map
936// ============================================================
937
938fn vgc_find_span(ptr voidptr) &VGC_Span {
939 addr := usize(ptr)
940 arena_idx := C.vgc_addr_to_arena(addr)
941 if arena_idx < 0 || arena_idx >= vgc_heap.narenas {
942 return unsafe { nil }
943 }
944 a := unsafe { &vgc_heap.arenas[arena_idx] }
945 if addr < a.base || addr >= a.base + a.size {
946 return unsafe { nil }
947 }
948 page_idx := (addr - a.base) / vgc_page_size
949 if page_idx < vgc_pages_per_arena {
950 return a.page_span[page_idx]
951 }
952 return unsafe { nil }
953}
954
955// Get the allocation size of an object
956fn vgc_get_obj_size(ptr voidptr) usize {
957 span := vgc_find_span(ptr)
958 if span == unsafe { nil } {
959 return 0
960 }
961 return usize(span.elem_size)
962}
963
964// Check if an address is within the GC heap - O(1) with fast bounds reject
965fn vgc_is_heap_ptr(addr usize) bool {
966 // Fast reject: most words on the stack are NOT heap pointers
967 if addr < vgc_arena_lo || addr >= vgc_arena_hi {
968 return false
969 }
970 arena_idx := C.vgc_addr_to_arena(addr)
971 if arena_idx < 0 || arena_idx >= vgc_heap.narenas {
972 return false
973 }
974 a := unsafe { &vgc_heap.arenas[arena_idx] }
975 return addr >= a.base && addr < a.base + a.used
976}
977
978// Safepoint: called when GC needs threads to stop
979fn vgc_safepoint() {
980 cache_idx := C.vgc_get_cache_idx()
981 if cache_idx < 0 {
982 return
983 }
984 // Update the live stack range for root scanning.
985 vgc_refresh_stack_range_for_sp(cache_idx, usize(C.vgc_get_sp()))
986 // Mark ourselves as stopped
987 C.vgc_atomic_store_u32(&vgc_heap.caches[cache_idx].stopped, 1)
988 C.vgc_atomic_add_u32(&vgc_heap.gc_stopped_count, 1)
989
990 // Wait until GC is done with stop-the-world phase
991 for C.vgc_atomic_load_u32(&vgc_heap.gc_stop_flag) != 0 {
992 C.vgc_atomic_fence()
993 }
994
995 C.vgc_atomic_store_u32(&vgc_heap.caches[cache_idx].stopped, 0)
996}
997