diff --git a/README.md b/README.md index b86d9eb..91e7cfa 100644 --- a/README.md +++ b/README.md @@ -3,68 +3,36 @@ # mm-go Generic manual memory management for golang -Golang manages memory via GC and it's good for almost every use case but sometimes it can be a bottleneck. -and this is where mm-go comes in to play. - - [mm-go Generic manual memory management for golang](#mm-go-generic-manual-memory-management-for-golang) - [Before using mm-go](#before-using-mm-go) - [Installing](#installing) - [Packages](#packages) + - [Allocators](#allocators) + - [C allocator](#c-allocator) + - [BatchAllocator](#batchallocator) + - [Generic Helpers](#generic-helpers) + - [Alloc/Free](#allocfree) + - [AllocMany/FreeMany](#allocmanyfreemany) + - [ReAlloc](#realloc) - [typedarena](#typedarena) - - [Alloc/Free](#allocfree) - - [AllocMany/FreeMany](#allocmanyfreemany) - - [ReAlloc](#realloc) - - [hashmap](#hashmap) - - [Methods](#methods) - - [New](#new) - - [Insert](#insert) - - [Delete](#delete) - - [Get](#get) - - [GetPtr](#getptr) - - [Free](#free) + - [Why does this exists while there is BatchAllocator?](#why-does-this-exists-while-there-is-batchallocator) - [vector](#vector) - - [Methods](#methods) - - [New](#new) - - [Init](#init) - - [Push](#push) - - [Pop](#pop) - - [Len](#len) - - [Cap](#cap) - - [Slice](#slice) - - [Last](#last) - - [At](#at) - - [AtPtr](#atptr) - - [Free](#free) - - [linkedlist](#linkedlist) - - [Methods](#methods) - - [New](#new) - - [PushBack](#pushback) - - [PushFront](#pushfront) - - [PopBack](#popback) - - [PopFront](#popfront) - - [ForEach](#foreach) - - [At](#at) - - [AtPtr](#atptr) - - [RemoveAt](#removeat) - - [Remove](#remove) - - [RemoveAll](#removeall) - - [FindIndex](#findindex) - - [FindIndexes](#findindexes) - - [Len](#len) - - [Free](#free) - [Benchmarks](#benchmarks) +Golang manages memory via GC and it's good for almost every use case but sometimes it can be a bottleneck. +and this is where mm-go comes in to play. + ## Before using mm-go - Golang doesn't have any way to manually allocate/free memory, so how does mm-go allocate/free? - It does so via cgo. + It does so via **cgo**. - Before considering using this try to optimize your program to use less pointers, as golang GC most of the time performs worse when there is a lot of pointers, if you can't use this lib. -- Manual memory management provides better performance (most of the time) but you are 100% responsible for managing it (bugs, segfaults, use after free, double free, ....) -- Don't mix Manually and Managed memory (example if you put a slice in a manually managed struct it will get collected because go GC doesn't see the manually allocated struct, use Vector instead) -- All data structures provided by the package are manually managed and thus can be safely included in manually managed structs without the GC freeing them, but you have to free them yourself! -- Try to minimize calls to cgo by preallocating (using Arena/AllocMany). +- Manual memory management provides better performance (most of the time) but you are **100% responsible** for managing it (bugs, segfaults, use after free, double free, ....) +- **Don't mix** Manually and Managed memory (example if you put a slice in a manually managed struct it will get collected because go GC doesn't see the manually allocated struct, use Vector instead) +- All data structures provided by the package are manually managed and thus can be safely included in manually managed structs without the GC freeing them, but **you have to free them yourself!** +- Try to minimize calls to cgo by preallocating (using batchallocator/Arena/AllocMany). - Check the docs, test files and read the README. ## Installing @@ -87,392 +55,283 @@ go get -u github.com/joetifa2003/mm-go `malloc` - contains wrappers to raw C malloc and free. -## typedarena +`allocator` - contains the Allocator interface and the C allocator implementation. -New creates a typed arena with the specified chunk size. -a chunk is the the unit of the arena, if T is int for example and the -chunk size is 5, then each chunk is going to hold 5 ints. And if the -chunk is filled it will allocate another chunk that can hold 5 ints. -then you can call FreeArena and it will deallocate all chunks together. -Using this will simplify memory management. +`batchallocator` - contains implementation an allocator, this can be used as an arena, and a way to reduce CGO overhead. -```go -arena := typedarena.New[int](3) // 3 is the chunk size which gets preallocated, if you allocated more than 3 it will preallocate another chunk of 3 T -defer arena.Free() // freeing the arena using defer to prevent leaks - -int1 := arena.Alloc() // allocates 1 int from arena -*int1 = 1 // changing it's value -ints := arena.AllocMany(2) // allocates 2 ints from the arena and returns a slice representing the heap (instead of pointer arithmetic) -ints[0] = 2 // changing the first value -ints[1] = 3 // changing the second value +## Allocators -// you can also take pointers from the slice -intPtr1 := &ints[0] // taking pointer from the manually managed heap -*intPtr1 = 15 // changing the value using pointers +Allocator is an interface that defines some methods needed for most allocators. -assert.Equal(1, *int1) -assert.Equal(2, len(ints)) -assert.Equal(15, ints[0]) -assert.Equal(3, ints[1]) +```go +Alloc(size int) unsafe.Pointer // returns a pointer to the allocated memory +Free(ptr unsafe.Pointer) // frees the memory pointed by ptr +Realloc(ptr unsafe.Pointer, size int) unsafe.Pointer // reallocates the memory pointed by ptr +Destroy() // any cleanup that the allocator needs to do ``` -## Alloc/Free +Currently there is two allocators implemented. -Alloc is a generic function that allocates T and returns a pointer to it that you can free later using Free +### C allocator -```go -ptr := mm.Alloc[int]() // allocates a single int and returns a ptr to it -defer mm.Free(ptr) // frees the int (defer recommended to prevent leaks) - -assert.Equal(0, *ptr) // allocations are zeroed by default -*ptr = 15 // changes the value using the pointer -assert.Equal(15, *ptr) -``` +The C allocator is using CGO underthehood to call calloc, realloc and free. ```go -type Node struct { - value int -} - -ptr := mm.Alloc[Node]() // allocates a single Node struct and returns a ptr to it -defer mm.Free(ptr) // frees the struct (defer recommended to prevent leaks) -``` +alloc := allocator.NewC() +defer alloc.Destroy() -## AllocMany/FreeMany +ptr := allocator.Alloc[int](alloc) +defer allocator.Free(alloc, ptr) -AllocMany is a generic function that allocates n of T and returns a slice that represents the heap (instead of pointer arithmetic => slice indexing) that you can free later using FreeMany +*ptr = 15 +``` -```go -allocated := mm.AllocMany[int](2) // allocates 2 ints and returns it as a slice of ints with length 2 -defer mm.FreeMany(allocated) // it's recommended to make sure the data gets deallocated (defer recommended to prevent leaks) -assert.Equal(2, len(allocated)) -allocated[0] = 15 // changes the data in the slice (aka the heap) -ptr := &allocated[0] // takes a pointer to the first int in the heap -// Be careful if you do ptr := allocated[0] this will take a copy from the data on the heap -*ptr = 45 // changes the value from 15 to 45 +### BatchAllocator -assert.Equal(45, allocated[0]) -``` +This allocator purpose is to reduce the overhead of calling CGO on every allocation/free, it also acts as an arena since it frees all the memory when `Destroy` is called. -## ReAlloc +Instead, it allocats large chunks of memory at once and then divides them when you allocate, making it much faster. -Reallocate reallocates memory allocated with AllocMany and doesn't change underling data +This allocator has to take another allocator for it to work, usually with the C allocator. ```go -allocated := mm.AllocMany[int](2) // allocates 2 int and returns it as a slice of ints with length 2 -allocated[0] = 15 -assert.Equal(2, len(allocated)) -allocated = mm.Reallocate(allocated, 3) -assert.Equal(3, len(allocated)) -assert.Equal(15, allocated[0]) // data after reallocation stays the same -mm.FreeMany(allocated) // didn't use defer here because i'm doing a reallocation and changing the value of allocated variable (otherwise can segfault) -``` - -## hashmap +alloc := batchallocator.New(allocator.NewC()) +defer alloc.Destroy() -Manually managed hashmap, keys can be hashmap.String, hashmap.Int or any type that implements the hashmap.Hashable interface +ptr := allocator.Alloc[int](alloc) +defer allocator.Free(alloc, ptr) -```go -type Hashable interface { - comparable - Hash() uint32 -} +*ptr = 15 ``` -You can use pkg like [hashstructure](https://github.com/mitchellh/hashstructure) to hash your complex types and implement the interface. +With this allocator, calling `Free/FreeMany` on pointers allocated with `Alloc/AllocMany` is optional, since when you call `Destroy` all memory is freed by default. -### Methods +But if you call `Free` the memory will be freed, so it acts as both Slab allocator and an Arena. -#### New +You can specify the size of chunks that are allocated by using options. ```go -// New creates a new Hashmap with key of type K and value of type V -func New[K Hashable, V any]() *Hashmap[K, V] +alloc := batchallocator.New(allocator.NewC(), + batchallocator.WithBucketSize(mm.SizeOf[int]()*15), +) ``` -#### Insert +For example this configures the batch allocator to allocate at minimum 15 ints at a time (by default it allocates ` page, which is usually 4kb). -```go -// Insert inserts a new value V if key K doesn't exist, -// Otherwise update the key K with value V -func (hm *Hashmap[K, V]) Insert(key K, value V) -``` +You can also allocate more than this configured amount in one big allocation, and it will work fine, unlike `typedarena`, more on that later. -#### Delete +### Generic Helpers -```go -// Delete delete value with key K -func (hm *Hashmap[K, V]) Delete(key K) -``` +As you saw in the examples above, there are some helper functions that automatically detrimine the size of the type you want to allocate, and it also automatically does type casting from `unsafe.Pointer`. -#### Get +So instead of doing this: ```go -// Get takes key K and return value V -func (hm *Hashmap[K, V]) Get(key K) (value V, exists bool) -``` - -#### GetPtr +alloc := batchallocator.New(allocator.NewC()) +defer alloc.Destroy() -```go -// GetPtr takes key K and return a pointer to value V -func (hm *Hashmap[K, V]) GetPtr(key K) (value *V, exists bool) +ptr := (*int)(alloc.Alloc(int(unsafe.Sizeof(int)))) +defer alloc.Free(unsafe.Pointer(ptr)) ``` -#### Free +You can do this: ```go -// Free frees the Hashmap -func (hm *Hashmap[K, V]) Free() -``` - -## vector +alloc := batchallocator.New(allocator.NewC()) +defer alloc.Destroy() -A contiguous growable array type. -You can think of the Vector as a manually managed slice that you can put in manually managed structs, if you put a slice in a manually managed struct it will get collected because go GC doesn't see the manually allocated struct. +ptr := allocator.Alloc[int](alloc) +defer allocator.Free(alloc, ptr) +``` -```go -v := vector.New[int]() -defer v.Free() +Yes, go doesn't have generic on pointer receivers, so these had to be implemented as functions. -v.Push(1) -v.Push(2) -v.Push(3) +#### Alloc/Free -assert.Equal(3, v.Len()) -assert.Equal(4, v.Cap()) -assert.Equal([]int{1, 2, 3}, v.Slice()) -assert.Equal(3, v.Pop()) -assert.Equal(2, v.Pop()) -assert.Equal(1, v.Pop()) -``` +Alloc is a generic function that allocates T and returns a pointer to it that you can free later using Free ```go -v := vector.New[int](5) -defer v.Free() +alloc := batchallocator.New(allocator.NewC()) +defer alloc.Destroy() -assert.Equal(5, v.Len()) -assert.Equal(5, v.Cap()) -``` - -```go -v := vector.New[int](5, 6) -defer v.Free() +ptr := allocator.Alloc[int](alloc) // allocates a single int and returns a ptr to it +defer allocator.Free(alloc, ptr) // frees the int (defer recommended to prevent leaks) -assert.Equal(5, v.Len()) -assert.Equal(6, v.Cap()) +assert.Equal(0, *ptr) // allocations are zeroed by default +*ptr = 15 // changes the value using the pointer +assert.Equal(15, *ptr) ``` ```go -v := vector.Init(1, 2, 3) -defer v.Free() - -assert.Equal(3, v.Len()) -assert.Equal(3, v.Cap()) +type Node struct { + value int +} -assert.Equal(3, v.Pop()) -assert.Equal(2, v.Pop()) -assert.Equal(1, v.Pop()) +alloc := batchallocator.New(allocator.NewC()) +ptr := allocator.Alloc[Node](alloc) // allocates a single Node struct and returns a ptr to it +defer allocator.Free(alloc, ptr) // frees the struct (defer recommended to prevent leaks) ``` -### Methods +#### AllocMany/FreeMany -#### New +AllocMany is a generic function that allocates n of T and returns a slice that represents the heap (instead of pointer arithmetic => slice indexing) that you can free later using FreeMany ```go -// New creates a new empty vector, if args not provided -// it will create an empty vector, if only one arg is provided -// it will init a vector with len and cap equal to the provided arg, -// if two args are provided it will init a vector with len = args[0] cap = args[1] -func New[T any](args ...int) *Vector[T] -``` +alloc := allocator.NewC() +defer allocator.Destroy() -#### Init +heap := allocator.AllocMany[int](alloc, 2) // allocates 2 ints and returns it as a slice of ints with length 2 +defer allocator.FreeMany(heap) // it's recommended to make sure the data gets deallocated (defer recommended to prevent leaks) -```go -// Init initializes a new vector with the T elements provided and sets -// it's len and cap to len(values) -func Init[T any](values ...T) *Vector[T] -``` - -#### Push +assert.Equal(2, len(heap)) +heap[0] = 15 // changes the data in the slice (aka the heap) +ptr := &heap[0] // takes a pointer to the first int in the heap +// Be careful if you do ptr := heap[0] this will take a copy from the data on the heap +*ptr = 45 // changes the value from 15 to 45 -```go -// Push pushes value T to the vector, grows if needed. -func (v *Vector[T]) Push(value T) +assert.Equal(45, heap[0]) +assert.Equal(0, heap[1]) ``` -#### Pop +WARNING: Do not append to the slice, this is only used to avoid pointer arithmetic and unsafe code. -```go -// Pop pops value T from the vector and returns it -func (v *Vector[T]) Pop() T -``` +### ReAlloc -#### Len +Reallocate reallocates memory allocated with AllocMany and doesn't change underling data ```go -// Len gets vector length -func (v *Vector[T]) Len() int -``` +alloc := allocator.NewC() +defer alloc.Destroy() -#### Cap +heap := allocator.AllocMany[int](alloc, 2) // allocates 2 int and returns it as a slice of ints with length 2 +heap[0] = 15 +assert.Equal(2, len(heap)) -```go -// Cap gets vector capacity (underling memory length). -func (v *Vector[T]) Cap() int -``` +heap = allocator.Realloc(allocated, 3) -#### Slice +assert.Equal(3, len(heap)) +assert.Equal(15, heap[0]) // data after reallocation stays the same -```go -// Slice gets a slice representing the vector -// CAUTION: don't append to this slice, this is only used -// if you want to loop on the vec elements -func (v *Vector[T]) Slice() []T +allocator.FreeMany(heap) // didn't use defer here because i'm doing a reallocation and changing the value of allocated variable (otherwise can segfault) ``` -#### Last -```go -// Last gets the last element from a vector -func (v *Vector[T]) Last() T -``` +## typedarena -#### At +New creates a typed arena with the specified chunk size. +a chunk is the the unit of the arena, if T is int for example and the +chunk size is 5, then each chunk is going to hold 5 ints. And if the +chunk is filled it will allocate another chunk that can hold 5 ints. +then you can call FreeArena and it will deallocate all chunks together. +Using this will simplify memory management. ```go -// At gets element T at specified index -func (v *Vector[T]) At(idx int) T -``` +alloc := allocator.NewC() +defer alloc.Destroy() -#### AtPtr +arena := typedarena.New[int](alloc, 3) // 3 is the chunk size which gets preallocated, if you allocated more than 3 it will preallocate another chunk of 3 T +defer arena.Free() // freeing the arena using defer to prevent leaks -```go -// AtPtr gets element a pointer of T at specified index -func (v *Vector[T]) AtPtr(idx int) *T -``` +int1 := arena.Alloc() // allocates 1 int from arena +*int1 = 1 // changing it's value +ints := arena.AllocMany(2) // allocates 2 ints from the arena and returns a slice representing the heap (instead of pointer arithmetic) +ints[0] = 2 // changing the first value +ints[1] = 3 // changing the second value -#### Free +// you can also take pointers from the slice +intPtr1 := &ints[0] // taking pointer from the manually managed heap +*intPtr1 = 15 // changing the value using pointers -```go -// Free deallocats the vector -func (v *Vector[T]) Free() +assert.Equal(1, *int1) +assert.Equal(2, len(ints)) +assert.Equal(15, ints[0]) +assert.Equal(3, ints[1]) ``` -## linkedlist - -LinkedList a doubly-linked list. -Note: can be a lot slower than Vector but sometimes faster in specific use cases +### Why does this exists while there is BatchAllocator? -### Methods - -#### New +- `typedarena` is much faster because it only works with one single type. +- `batchallocator` is more generic and works for any type, even multiple types all at once. ```go -// New creates a new linked list. -func New[T any]() *LinkedList[T] -``` - -#### PushBack +// You cannot do this with `typedarena` because it only works with one single type. -```go -// PushBack pushes value T to the back of the linked list. -func (ll *LinkedList[T]) PushBack(value T) -``` +alloc := batchallocator.New(allocator.NewC()) +defer alloc.Destroy() -#### PushFront +i := allocator.Alloc[int](alloc) +s := allocator.Alloc[string](alloc) +x := allocator.Alloc[float64](alloc) -```go -// PushFront pushes value T to the back of the linked list. -func (ll *LinkedList[T]) PushFront(value T) +// they are all freed automatically because of Destroy above ``` -#### PopBack +- `batchallocator` can be passed to multiple data structures, like `vector` and `hashmap` and it will be automatically Freed when `Destroy` is called. -```go -// PopBack pops and returns value T from the back of the linked list. -func (ll *LinkedList[T]) PopBack() T -``` - -#### PopFront +- Also `typedarena.AllocMany` cannot exceed chunk size, but with `batchallocator` you can request any amount of memory. ```go -// PopFront pops and returns value T from the front of the linked list. -func (ll *LinkedList[T]) PopFront() T -``` +alloc := allocator.NewC() +defer alloc.Destroy() -#### ForEach +arena := typedarena.New[int](alloc, 3) -```go -// ForEach iterates through the linked list. -func (ll *LinkedList[T]) ForEach(f func(idx int, value T)) +heap := arena.AllocMany(2) // fine +heap2 := arena.AllocMany(2) // also fine +heap2 := arena.AllocMany(5) // panics ``` -#### At -```go -// At gets value T at idx. -func (ll *LinkedList[T]) At(idx int) T -``` +## vector -#### AtPtr +A contiguous growable array type. +You can think of the Vector as a manually managed slice that you can put in manually managed structs, if you put a slice in a manually managed struct it will get collected because go GC doesn't see the manually allocated struct. ```go -// AtPtr gets a pointer to value T at idx. -func (ll *LinkedList[T]) AtPtr(idx int) *T -``` +v := vector.New[int]() +defer v.Free() -#### RemoveAt +v.Push(1) +v.Push(2) +v.Push(3) -```go -// RemoveAt removes value T at specified index and returns it. -func (ll *LinkedList[T]) RemoveAt(idx int) T +assert.Equal(3, v.Len()) +assert.Equal(4, v.Cap()) +assert.Equal([]int{1, 2, 3}, v.Slice()) +assert.Equal(3, v.Pop()) +assert.Equal(2, v.Pop()) +assert.Equal(1, v.Pop()) ``` -#### Remove - ```go -// Remove removes the first value T that pass the test implemented by the provided function. -// if the test function succeeded it will return the value and true -func (ll *LinkedList[T]) Remove(f func(idx int, value T) bool) (value T, ok bool) -``` - -#### RemoveAll +v := vector.New[int](5) +defer v.Free() -```go -// RemoveAll removes all values of T that pass the test implemented by the provided function. -func (ll *LinkedList[T]) RemoveAll(f func(idx int, value T) bool) []T +assert.Equal(5, v.Len()) +assert.Equal(5, v.Cap()) ``` -#### FindIndex - ```go -// FindIndex returns the first index of value T that pass the test implemented by the provided function. -func (ll *LinkedList[T]) FindIndex(f func(value T) bool) (idx int, ok bool) -``` - -#### FindIndexes +v := vector.New[int](5, 6) +defer v.Free() -```go -// FindIndex returns all indexes of value T that pass the test implemented by the provided function. -func (ll *LinkedList[T]) FindIndexes(f func(value T) bool) []int +assert.Equal(5, v.Len()) +assert.Equal(6, v.Cap()) ``` -#### Len - ```go -// Len gets linked list length. -func (ll *LinkedList[T]) Len() int -``` +v := vector.Init(1, 2, 3) +defer v.Free() -#### Free +assert.Equal(3, v.Len()) +assert.Equal(3, v.Cap()) -```go -// Free frees the linked list. -func (ll *LinkedList[T]) Free() +assert.Equal(3, v.Pop()) +assert.Equal(2, v.Pop()) +assert.Equal(1, v.Pop()) ``` + ## Benchmarks Check the test files and github actions for the benchmarks (linux, macos, windows).