package arena import ( "errors" "log" "math/bits" "runtime" "sync" "unsafe" "git.brut.systems/judah/xx/mem" ) // Linear is a simple bump allocator with a fixed amount of backing memory. func Linear(capacity_in_bytes uintptr) Arena { if capacity_in_bytes <= 0 { panic("linear: capacity_in_bytes must be greater than zero") } var ( data = make([]byte, capacity_in_bytes) offset uintptr ) return func(a Action, size, align uintptr, watermark *uintptr) (unsafe.Pointer, error) { switch a { case ACTION_ALLOC: aligned := mem.AlignForward(size, align) if offset+aligned > capacity_in_bytes { return nil, errors.New("linear: out of memory") } ptr := &data[offset] offset += aligned return unsafe.Pointer(ptr), nil case ACTION_RESET: clear(data) offset = 0 case ACTION_SAVE: if watermark == nil { return nil, errors.New("linear: cannot save to nil watermark") } *watermark = offset case ACTION_RESTORE: if watermark == nil { return nil, errors.New("linear: cannot restore nil watermark") } clear(data[*watermark:offset]) offset = *watermark default: panic("linear: unimplemented action - " + a.String()) } return nil, nil } } // Ring is an Arena that only allocates values of the given type. // When capacity is exceeded, previous allocations will be reused to accommodate new ones // // Note: Allocating different types from the same Pool is unsafe and may cause memory corruption. func Ring[T any](capacity uintptr) Arena { if capacity <= 0 { panic("pool: capacity must be greater than zero") } pointers := make([]T, 0, capacity) return func(a Action, _, _ uintptr, watermark *uintptr) (unsafe.Pointer, error) { switch a { case ACTION_ALLOC: if len(pointers) == cap(pointers) { pointers = pointers[:0] } pointers = append(pointers, mem.ZeroValue[T]()) return unsafe.Pointer(&pointers[len(pointers)-1]), nil case ACTION_RESET: clear(pointers) pointers = pointers[:0] case ACTION_SAVE: if watermark == nil { return nil, errors.New("pool: cannot save to nil watermark") } *watermark = uintptr(len(pointers)) case ACTION_RESTORE: if watermark == nil { return nil, errors.New("pool: cannot restore nil watermark") } clear(pointers[*watermark:]) pointers = pointers[:*watermark] default: panic("pool: unimplemented action - " + a.String()) } return nil, nil } } // Chunked is an Arena that groups allocations by size. func Chunked(max_allocs_per_chunk uintptr) Arena { type chunk struct { data []byte offset uintptr saved uintptr } groups := make([][]chunk, 64) return func(a Action, size, align uintptr, _ *uintptr) (unsafe.Pointer, error) { switch a { case ACTION_ALLOC: aligned := mem.AlignForward(size, align) if aligned == 0 { aligned = 1 } aligned = 1 << bits.Len(uint(aligned-1)) idx := bits.TrailingZeros(uint(aligned)) if idx >= len(groups) { groups = append(groups, make([][]chunk, idx-len(groups)+1)...) } group := groups[idx] if len(group) == 0 { group = append(group, chunk{ data: make([]byte, aligned*max_allocs_per_chunk), }) } c := &group[len(group)-1] if c.offset+aligned > uintptr(len(c.data)) { group = append(group, chunk{ data: make([]byte, aligned*max_allocs_per_chunk), }) c = &group[len(group)-1] } ptr := &c.data[c.offset] c.offset += aligned groups[idx] = group return unsafe.Pointer(ptr), nil case ACTION_RESET: for _, g := range groups { for i := range len(g) { g[i].offset = 0 g[i].saved = 0 clear(g[i].data) } } case ACTION_SAVE: for _, g := range groups { for i := range len(g) { g[i].saved = g[i].offset } } case ACTION_RESTORE: for _, g := range groups { for i := range len(g) { g[i].offset = g[i].saved } } default: panic("chunked: unimplemented action - " + a.String()) } return nil, nil } } // Nil is an Arena that always returns an error. // // Note: This is useful for tracking usage locations func Nil() Arena { return func(a Action, size, align uintptr, watermark *uintptr) (unsafe.Pointer, error) { return nil, errors.New("use of nil allocator") } } // Region wraps an Arena, restoring it to its previous state when Reset is called. func Region(arena Arena) Arena { watermark := Save(arena) return func(a Action, size, align uintptr, wm *uintptr) (unsafe.Pointer, error) { if a == ACTION_RESET { Restore(arena, watermark) return nil, nil } return arena(a, size, align, wm) } } // Split wraps two [[Arena]]s, dispatching allocations to a particular one based on the requested size. func Split(split_size uintptr, smaller, larger Arena) Arena { var watermarks [2]uintptr return func(a Action, size, align uintptr, watermark *uintptr) (unsafe.Pointer, error) { switch a { case ACTION_ALLOC: if size <= split_size { return smaller(a, size, align, watermark) } return larger(a, size, align, watermark) case ACTION_RESET: Reset(smaller) Reset(larger) case ACTION_SAVE: watermarks[0] = Save(smaller) watermarks[1] = Save(larger) case ACTION_RESTORE: Restore(smaller, watermarks[0]) Restore(larger, watermarks[1]) default: panic("split: unimplemented action - " + a.String()) } return nil, nil } } // Logger wraps an Arena, logging its usage locations. func Logger(arena Arena) Arena { return func(a Action, size, align uintptr, watermark *uintptr) (unsafe.Pointer, error) { // We expect allocators to be used via the high-level API, so we grab the caller location relative to that. // @todo(judah): can we determine this dynamically? _, file, line, ok := runtime.Caller(2) if !ok { file = "" line = 0 } log.Printf("%s:%d - %s (size: %d, align: %d, watermark: %p)", file, line, a, size, align, watermark) return arena(a, size, align, watermark) } } // Concurrent wraps an Arena, ensuring it is safe for concurrent use. func Concurrent(arena Arena) Arena { mtx := new(sync.Mutex) return func(a Action, size, align uintptr, watermark *uintptr) (unsafe.Pointer, error) { mtx.Lock() ptr, err := arena(a, size, align, watermark) mtx.Unlock() return ptr, err } } // Pinned wraps an Arena, ensuring the memory returned is stable until Reset is called. // // The memory returned by Pinned is safe to pass over cgo boundaries. func Pinned(arena Arena) Arena { var pinner runtime.Pinner return func(a Action, size, align uintptr, watermark *uintptr) (unsafe.Pointer, error) { ptr, err := arena(a, size, align, watermark) if err != nil { return ptr, err } if a == ACTION_RESET { pinner.Unpin() } else { pinner.Pin(ptr) } return ptr, err } }