diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index f423722..132d44e 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -13,7 +13,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v2 with: - go-version: "~1.20.0" + go-version: "~1.23.0" - name: Install benchstat run: go install golang.org/x/perf/cmd/benchstat@latest diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 9f86c52..f13fc5c 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -11,7 +11,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v2 with: - go-version: "~1.19.0" + go-version: "~1.23.0" - name: Code coverage run: go test -cover ./... diff --git a/README.md b/README.md index 442154b..ce224eb 100644 --- a/README.md +++ b/README.md @@ -6,55 +6,15 @@ 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) - - [typedarena](#typedarena) - - [Alloc/Free](#allocfree) - - [AllocMany/FreeMany](#allocmanyfreemany) - - [ReAlloc](#realloc) - - [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-1) - - [New](#new-1) - - [PushBack](#pushback) - - [PushFront](#pushfront) - - [PopBack](#popback) - - [PopFront](#popfront) - - [ForEach](#foreach) - - [At](#at-1) - - [AtPtr](#atptr-1) - - [RemoveAt](#removeat) - - [Remove](#remove) - - [RemoveAll](#removeall) - - [FindIndex](#findindex) - - [FindIndexes](#findindexes) - - [Len](#len-1) - - [Free](#free-1) - - [Benchmarks](#benchmarks) - ## 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 @@ -63,374 +23,1803 @@ and this is where mm-go comes in to play. go get -u github.com/joetifa2003/mm-go ``` -## Packages +## At a glance + +```go +type MyStruct struct { + a int + b float32 +} + +func Example_datastructures() { + alloc := allocator.NewC() + defer alloc.Destroy() + + p := allocator.Alloc[MyStruct](alloc) + defer allocator.Free(alloc, p) + + p.a = 100 + p.b = 200 + + fmt.Println(*p) + + v := vector.New[int](alloc) + defer v.Free() + v.Push(15) + v.Push(70) + + for _, i := range v.Iter() { + fmt.Println(i) + } + + l := linkedlist.New[*mmstring.MMString](alloc) + defer l.Free() + l.PushBack(mmstring.From(alloc, "hello")) + l.PushBack(mmstring.From(alloc, "world")) + + for _, i := range l.Iter() { + fmt.Println(i.GetGoString()) + } + + // Output: + // {100 200} + // 15 + // 70 + // hello + // world +} +``` + +`mm-go` is built around the concept of Allocators, which is an interface that can be implemented and passed around to the library. +You use these allocators to allocate memory, and also allocate datastructures like vectors, linkedlists, hashmaps, etc. + +## Benchmarks + +Check the test files and github actions for the benchmarks (linux, macos, windows). +mm-go can sometimes be 5-10 times faster. + +``` +Run go test ./... -bench=. -count 5 > out.txt && benchstat out.txt -`mm` - basic generic memory management functions. -`typedarena` - contains TypedArena which allocates many objects and free them all at once. +goos: linux +goarch: amd64 +pkg: github.com/joetifa2003/mm-go +cpu: AMD Ryzen 7 5800H with Radeon Graphics + │ out.txt │ + │ sec/op │ +LinkedListManaged-16 605.7µ ± ∞ ¹ +LinkedListCAlloc-16 933.1µ ± ∞ ¹ +LinkedListBatchAllocator/bucket_size_100-16 513.3µ ± ∞ ¹ +LinkedListBatchAllocator/bucket_size_200-16 405.8µ ± ∞ ¹ +LinkedListBatchAllocator/bucket_size_500-16 425.4µ ± ∞ ¹ +LinkedListBatchAllocator/bucket_size_10000-16 200.7µ ± ∞ ¹ +LinkedListTypedArena/chunk_size_100-16 105.3µ ± ∞ ¹ +LinkedListTypedArena/chunk_size_200-16 95.50µ ± ∞ ¹ +LinkedListTypedArena/chunk_size_500-16 83.02µ ± ∞ ¹ +LinkedListTypedArena/chunk_size_10000-16 75.96µ ± ∞ ¹ +geomean 240.1µ +¹ need >= 6 samples for confidence interval at level 0.95 + +pkg: github.com/joetifa2003/mm-go/hashmap + │ out.txt │ + │ sec/op │ +HashmapGo-16 210.7µ ± ∞ ¹ +HashmapCAlloc-16 189.1µ ± ∞ ¹ +HashmapBatchAlloc-16 118.2µ ± ∞ ¹ +geomean 167.6µ +¹ need >= 6 samples for confidence interval at level 0.95 -`vector` - contains a manually managed Vector implementation. +``` + + + + + +# mm -`linkedlist` - contains a manually managed Linkedlist implementation. +```go +import "github.com/joetifa2003/mm-go" +``` -`mmstring` - contains a manually managed string implementation. +## Index -`malloc` - contains wrappers to raw C malloc and free. +- [func SizeOf\[T any\]\(\) int](<#SizeOf>) +- [func Zero\[T any\]\(\) T](<#Zero>) -## typedarena -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. + +## func [SizeOf]() ```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 +func SizeOf[T any]() int +``` + +SizeOf returns the size of T in bytes -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 +
Example +

-// 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 -assert.Equal(1, *int1) -assert.Equal(2, len(ints)) -assert.Equal(15, ints[0]) -assert.Equal(3, ints[1]) + +```go +fmt.Println(mm.SizeOf[int32]()) +fmt.Println(mm.SizeOf[int64]()) +// Output: +// 4 +// 8 +``` + +#### Output + +``` +4 +8 ``` -## Alloc/Free +

+
-Alloc is a generic function that allocates T and returns a pointer to it that you can free later using Free + +## func [Zero]() ```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) +func Zero[T any]() T +``` + +Zero returns a zero value of T -assert.Equal(0, *ptr) // allocations are zeroed by default -*ptr = 15 // changes the value using the pointer -assert.Equal(15, *ptr) +# allocator + +```go +import "github.com/joetifa2003/mm-go/allocator" ``` +
Example +

+ + + ```go -type Node struct { - value int +package main + +import ( + "fmt" + + "github.com/joetifa2003/mm-go/allocator" +) + +func main() { + alloc := allocator.NewC() + defer alloc.Destroy() + + ptr := allocator.Alloc[int](alloc) + defer allocator.Free(alloc, ptr) + + *ptr = 15 + fmt.Println(*ptr) + } +``` -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) +#### Output + +``` +15 ``` -## AllocMany/FreeMany +

+
+ +
Example (Datastructures) +

+ -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 -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 +package main + +import ( + "fmt" + + "github.com/joetifa2003/mm-go/allocator" + "github.com/joetifa2003/mm-go/linkedlist" + "github.com/joetifa2003/mm-go/mmstring" + "github.com/joetifa2003/mm-go/vector" +) + +type MyStruct struct { + a int + b float32 +} + +func main() { + alloc := allocator.NewC() + defer alloc.Destroy() // all the memory allocated bellow will be freed, no need to free it manually. + + p := allocator.Alloc[MyStruct](alloc) + defer allocator.Free(alloc, p) + + p.a = 100 + p.b = 200 + + fmt.Println(*p) + + v := vector.New[int](alloc) + defer v.Free() + v.Push(15) + v.Push(70) + + for _, i := range v.Iter() { + fmt.Println(i) + } + + l := linkedlist.New[*mmstring.MMString](alloc) + defer l.Free() + l.PushBack(mmstring.From(alloc, "hello")) + l.PushBack(mmstring.From(alloc, "world")) + + for _, i := range l.Iter() { + fmt.Println(i.GetGoString()) + } + +} +``` + +#### Output -assert.Equal(45, allocated[0]) ``` +{100 200} +15 +70 +hello +world +``` + +

+
+ +## Index + +- [func Alloc\[T any\]\(a Allocator\) \*T](<#Alloc>) +- [func AllocMany\[T any\]\(a Allocator, n int\) \[\]T](<#AllocMany>) +- [func Free\[T any\]\(a Allocator, ptr \*T\)](<#Free>) +- [func FreeMany\[T any\]\(a Allocator, slice \[\]T\)](<#FreeMany>) +- [func Realloc\[T any\]\(a Allocator, slice \[\]T, newN int\) \[\]T](<#Realloc>) +- [type Allocator](<#Allocator>) + - [func NewAllocator\(allocator unsafe.Pointer, alloc func\(allocator unsafe.Pointer, size int\) unsafe.Pointer, free func\(allocator unsafe.Pointer, ptr unsafe.Pointer\), realloc func\(allocator unsafe.Pointer, ptr unsafe.Pointer, size int\) unsafe.Pointer, destroy func\(allocator unsafe.Pointer\)\) Allocator](<#NewAllocator>) + - [func NewC\(\) Allocator](<#NewC>) + - [func \(a Allocator\) Alloc\(size int\) unsafe.Pointer](<#Allocator.Alloc>) + - [func \(a Allocator\) Destroy\(\)](<#Allocator.Destroy>) + - [func \(a Allocator\) Free\(ptr unsafe.Pointer\)](<#Allocator.Free>) + - [func \(a Allocator\) Realloc\(ptr unsafe.Pointer, size int\) unsafe.Pointer](<#Allocator.Realloc>) -## ReAlloc -Reallocate reallocates memory allocated with AllocMany and doesn't change underling data + +## func [Alloc]() ```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) +func Alloc[T any](a Allocator) *T ``` -## vector +Alloc allocates T and returns a pointer to it. + +
Example +

+ -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 -v := vector.New[int]() -defer v.Free() +alloc := allocator.NewC() +defer alloc.Destroy() + +// So you can do this: +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) +*ptr = 15 +fmt.Println(*ptr) + +// instead of doing this: +ptr2 := (*int)(alloc.Alloc(mm.SizeOf[int]())) +defer alloc.Free(unsafe.Pointer(ptr2)) +*ptr2 = 15 -v.Push(1) -v.Push(2) -v.Push(3) +fmt.Println(*ptr2) -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()) +// Output: +// 15 +// 15 ``` +#### Output + +``` +15 +15 +``` + +

+
+ + +## func [AllocMany]() + +```go +func AllocMany[T any](a Allocator, n int) []T +``` + +AllocMany allocates n of T and returns a slice representing the heap. CAUTION: don't append to the slice, the purpose of it is to replace pointer arithmetic with slice indexing + +
Example +

+ + + ```go -v := vector.New[int](5) -defer v.Free() +package main + +import ( + "fmt" + + "github.com/joetifa2003/mm-go/allocator" +) + +func main() { + alloc := allocator.NewC() + defer alloc.Destroy() -assert.Equal(5, v.Len()) -assert.Equal(5, v.Cap()) + heap := allocator.AllocMany[int](alloc, 2) // allocates 2 ints and returns it as a slice of ints with length 2 + defer allocator.FreeMany(alloc, heap) // it's recommended to make sure the data gets deallocated (defer recommended to prevent leaks) + + 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 + heap[1] = 70 + + fmt.Println(heap[0]) + fmt.Println(heap[1]) + +} +``` + +#### Output + +``` +45 +70 ``` +

+
+ + +## func [Free]() + +```go +func Free[T any](a Allocator, ptr *T) +``` + +FreeMany frees memory allocated by Alloc takes a ptr CAUTION: be careful not to double free, and prefer using defer to deallocate + + +## func [FreeMany]() + ```go -v := vector.New[int](5, 6) -defer v.Free() +func FreeMany[T any](a Allocator, slice []T) +``` -assert.Equal(5, v.Len()) -assert.Equal(6, v.Cap()) +FreeMany frees memory allocated by AllocMany takes in the slice \(aka the heap\) CAUTION: be careful not to double free, and prefer using defer to deallocate + + +## func [Realloc]() + +```go +func Realloc[T any](a Allocator, slice []T, newN int) []T ``` +Realloc reallocates memory allocated with AllocMany and doesn't change underling data + +
Example +

+ + + ```go -v := vector.Init(1, 2, 3) -defer v.Free() +package main + +import ( + "fmt" + + "github.com/joetifa2003/mm-go/allocator" +) + +func main() { + alloc := allocator.NewC() + defer alloc.Destroy() + + heap := allocator.AllocMany[int](alloc, 2) // allocates 2 int and returns it as a slice of ints with length 2 + + heap[0] = 15 + heap[1] = 70 + + heap = allocator.Realloc(alloc, heap, 3) + heap[2] = 100 + + fmt.Println(heap[0]) + fmt.Println(heap[1]) + fmt.Println(heap[2]) + + allocator.FreeMany(alloc, heap) + +} +``` -assert.Equal(3, v.Len()) -assert.Equal(3, v.Cap()) +#### Output -assert.Equal(3, v.Pop()) -assert.Equal(2, v.Pop()) -assert.Equal(1, v.Pop()) ``` +15 +70 +100 +``` + +

+
-### Methods + +## type [Allocator]() -#### New +Allocator is an interface that defines some methods needed for most allocators. It's not a golang interface, so it's safe to use in manually managed structs \(will not get garbage collected\). ```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] +type Allocator struct { + // contains filtered or unexported fields +} ``` -#### Init + +### func [NewAllocator]() ```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] +func NewAllocator(allocator unsafe.Pointer, alloc func(allocator unsafe.Pointer, size int) unsafe.Pointer, free func(allocator unsafe.Pointer, ptr unsafe.Pointer), realloc func(allocator unsafe.Pointer, ptr unsafe.Pointer, size int) unsafe.Pointer, destroy func(allocator unsafe.Pointer)) Allocator ``` -#### Push +NewAllocator creates a new Allocator + +
Example +

+ + ```go -// Push pushes value T to the vector, grows if needed. -func (v *Vector[T]) Push(value T) +package main + +import ( + "unsafe" + + "github.com/joetifa2003/mm-go/allocator" +) + +func main() { + // Create a custom allocator + alloc := allocator.NewAllocator( + nil, + myallocator_alloc, + myallocator_free, + myallocator_realloc, + myallocator_destroy, + ) + + // Check how C allocator is implemented + // or batchallocator source for a reference + + _ = alloc +} + +func myallocator_alloc(allocator unsafe.Pointer, size int) unsafe.Pointer { + return nil +} + +func myallocator_free(allocator unsafe.Pointer, ptr unsafe.Pointer) { +} + +func myallocator_realloc(allocator unsafe.Pointer, ptr unsafe.Pointer, size int) unsafe.Pointer { + return nil +} + +func myallocator_destroy(allocator unsafe.Pointer) { +} ``` -#### Pop +

+
+ + +### func [NewC]() ```go -// Pop pops value T from the vector and returns it -func (v *Vector[T]) Pop() T +func NewC() Allocator ``` -#### Len +NewC returns an allocator that uses C calloc, realloc and free. + + +### func \(Allocator\) [Alloc]() ```go -// Len gets vector length -func (v *Vector[T]) Len() int +func (a Allocator) Alloc(size int) unsafe.Pointer ``` -#### Cap +Alloc allocates size bytes and returns an unsafe pointer to it. + + +### func \(Allocator\) [Destroy]() ```go -// Cap gets vector capacity (underling memory length). -func (v *Vector[T]) Cap() int +func (a Allocator) Destroy() ``` -#### Slice +Destroy destroys the allocator. After calling this, the allocator is no longer usable. This is useful for cleanup, freeing allocator internal resources, etc. + + +### func \(Allocator\) [Free]() ```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 +func (a Allocator) Free(ptr unsafe.Pointer) ``` -#### Last +Free frees the memory pointed by ptr + + +### func \(Allocator\) [Realloc]() ```go -// Last gets the last element from a vector -func (v *Vector[T]) Last() T +func (a Allocator) Realloc(ptr unsafe.Pointer, size int) unsafe.Pointer ``` -#### At +Realloc reallocates the memory pointed by ptr with a new size and returns a new pointer to it. + +# batchallocator ```go -// At gets element T at specified index -func (v *Vector[T]) At(idx int) T +import "github.com/joetifa2003/mm-go/batchallocator" ``` -#### AtPtr +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. It allocats large chunks of memory at once and then divides them when you allocate, making it much faster. This allocator has to take another allocator for it to work, usually with the C allocator. You can optionally call \`Free\` on the pointers allocated by batchallocator manually, and it will free the memory as soon as it can. \`Destroy\` must be called to free internal resources and free all the memory allocated by the allocator. + +
Example +

+ + ```go -// AtPtr gets element a pointer of T at specified index -func (v *Vector[T]) AtPtr(idx int) *T +package main + +import ( + "github.com/joetifa2003/mm-go/allocator" + "github.com/joetifa2003/mm-go/batchallocator" +) + +func main() { + alloc := batchallocator.New(allocator.NewC()) // by default it allocates page, which is usually 4kb + defer alloc.Destroy() // this frees all memory allocated by the allocator automatically + + ptr := allocator.Alloc[int](alloc) + // but you can still free the pointers manually if you want (will free buckets of memory if all pointers depending on it is freed) + defer allocator.Free(alloc, ptr) // this can removed and the memory will be freed. +} ``` -#### Free +

+
+ +
Example (Arena) +

+ + ```go -// Free deallocats the vector -func (v *Vector[T]) Free() +package main + +import ( + "fmt" + + "github.com/joetifa2003/mm-go/allocator" + "github.com/joetifa2003/mm-go/batchallocator" + "github.com/joetifa2003/mm-go/linkedlist" + "github.com/joetifa2003/mm-go/mmstring" + "github.com/joetifa2003/mm-go/vector" +) + +func main() { + alloc := batchallocator.New(allocator.NewC()) + defer alloc.Destroy() // all the memory allocated bellow will be freed, no need to free it manually. + + v := vector.New[int](alloc) + v.Push(15) + v.Push(70) + + for _, i := range v.Iter() { + fmt.Println(i) + } + + l := linkedlist.New[*mmstring.MMString](alloc) + l.PushBack(mmstring.From(alloc, "hello")) + l.PushBack(mmstring.From(alloc, "world")) + + for _, i := range l.Iter() { + fmt.Println(i.GetGoString()) + } + +} ``` -## linkedlist +#### Output -LinkedList a doubly-linked list. -Note: can be a lot slower than Vector but sometimes faster in specific use cases +``` +15 +70 +hello +world +``` + +

+
+ +## Index -### Methods +- [func New\(a allocator.Allocator, options ...BatchAllocatorOption\) allocator.Allocator](<#New>) +- [type BatchAllocator](<#BatchAllocator>) +- [type BatchAllocatorOption](<#BatchAllocatorOption>) + - [func WithBucketSize\(size int\) BatchAllocatorOption](<#WithBucketSize>) -#### New + + +## func [New]() ```go -// New creates a new linked list. -func New[T any]() *LinkedList[T] +func New(a allocator.Allocator, options ...BatchAllocatorOption) allocator.Allocator ``` -#### PushBack +New creates a new BatchAllocator and applies optional configuration using BatchAllocatorOption + + +## type [BatchAllocator]() + +BatchAllocator manages a collection of memory buckets to optimize small allocations ```go -// PushBack pushes value T to the back of the linked list. -func (ll *LinkedList[T]) PushBack(value T) +type BatchAllocator struct { + // contains filtered or unexported fields +} ``` -#### PushFront + +## type [BatchAllocatorOption]() + + ```go -// PushFront pushes value T to the back of the linked list. -func (ll *LinkedList[T]) PushFront(value T) +type BatchAllocatorOption func(alloc *BatchAllocator) ``` -#### PopBack + +### func [WithBucketSize]() ```go -// PopBack pops and returns value T from the back of the linked list. -func (ll *LinkedList[T]) PopBack() T +func WithBucketSize(size int) BatchAllocatorOption ``` -#### PopFront +WithBucketSize Option to specify bucket size when creating BatchAllocator You can allocate more memory than the bucketsize in one allocation, it will allocate a new bucket and put the data in it. + +
Example +

+ + ```go -// PopFront pops and returns value T from the front of the linked list. -func (ll *LinkedList[T]) PopFront() T +alloc := batchallocator.New( + allocator.NewC(), + batchallocator.WithBucketSize(mm.SizeOf[int]()*15), // configure the allocator to allocate size of 15 ints per bucket. +) +defer alloc.Destroy() + +ptr := allocator.Alloc[int](alloc) +defer allocator.Free(alloc, ptr) // this can be removed and the memory will still be freed on Destroy. + +ptr2 := allocator.Alloc[int](alloc) // will not call CGO because there is still enough memory in the Bucket. +defer allocator.Free(alloc, ptr2) // this can be removed and the memory will still be freed on Destroy. ``` -#### ForEach +

+
+ +# hashmap ```go -// ForEach iterates through the linked list. -func (ll *LinkedList[T]) ForEach(f func(idx int, value T)) +import "github.com/joetifa2003/mm-go/hashmap" ``` -#### At +
Example +

+ + ```go -// At gets value T at idx. -func (ll *LinkedList[T]) At(idx int) T +alloc := batchallocator.New(allocator.NewC()) +defer alloc.Destroy() + +hm := New[int, int](alloc) +defer hm.Free() // can be removed + +hm.Set(1, 10) +hm.Set(2, 20) +hm.Set(3, 30) + +sumKeys := 0 +sumValues := 0 +for k, v := range hm.Iter() { + sumKeys += k + sumValues += v +} + +fmt.Println(sumKeys) +fmt.Println(sumValues) + +// Output: +// 6 +// 60 +``` + +#### Output + +``` +6 +60 ``` -#### AtPtr +

+
+ +## Index + +- [type Hashmap](<#Hashmap>) + - [func New\[K comparable, V any\]\(alloc allocator.Allocator\) \*Hashmap\[K, V\]](<#New>) + - [func \(hm \*Hashmap\[K, V\]\) Delete\(key K\)](<#Hashmap[K, V].Delete>) + - [func \(hm \*Hashmap\[K, V\]\) Free\(\)](<#Hashmap[K, V].Free>) + - [func \(hm \*Hashmap\[K, V\]\) Get\(key K\) \(value V, exists bool\)](<#Hashmap[K, V].Get>) + - [func \(hm \*Hashmap\[K, V\]\) GetPtr\(key K\) \(value \*V, exists bool\)](<#Hashmap[K, V].GetPtr>) + - [func \(hm \*Hashmap\[K, V\]\) Iter\(\) iter.Seq2\[K, V\]](<#Hashmap[K, V].Iter>) + - [func \(hm \*Hashmap\[K, V\]\) Keys\(\) \[\]K](<#Hashmap[K, V].Keys>) + - [func \(hm \*Hashmap\[K, V\]\) Set\(key K, value V\)](<#Hashmap[K, V].Set>) + - [func \(hm \*Hashmap\[K, V\]\) Values\(\) \[\]V](<#Hashmap[K, V].Values>) + + + +## type [Hashmap]() + +Hashmap Manually managed hashmap, ```go -// AtPtr gets a pointer to value T at idx. -func (ll *LinkedList[T]) AtPtr(idx int) *T +type Hashmap[K comparable, V any] struct { + // contains filtered or unexported fields +} ``` -#### RemoveAt + +### func [New]() ```go -// RemoveAt removes value T at specified index and returns it. -func (ll *LinkedList[T]) RemoveAt(idx int) T +func New[K comparable, V any](alloc allocator.Allocator) *Hashmap[K, V] ``` -#### Remove +New creates a new Hashmap with key of type K and value of type V + + +### func \(\*Hashmap\[K, V\]\) [Delete]() ```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) +func (hm *Hashmap[K, V]) Delete(key K) ``` -#### RemoveAll +Delete delete value with key K + + +### func \(\*Hashmap\[K, 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 +func (hm *Hashmap[K, V]) Free() ``` -#### FindIndex +Free frees the Hashmap + + +### func \(\*Hashmap\[K, V\]\) [Get]() ```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) +func (hm *Hashmap[K, V]) Get(key K) (value V, exists bool) ``` -#### FindIndexes +Get takes key K and return value V + + +### func \(\*Hashmap\[K, V\]\) [GetPtr]() ```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 +func (hm *Hashmap[K, V]) GetPtr(key K) (value *V, exists bool) ``` -#### Len +GetPtr takes key K and return a pointer to value V + + +### func \(\*Hashmap\[K, V\]\) [Iter]() ```go -// Len gets linked list length. -func (ll *LinkedList[T]) Len() int +func (hm *Hashmap[K, V]) Iter() iter.Seq2[K, V] ``` -#### Free +Iter returns an iterator over all key/value pairs + + +### func \(\*Hashmap\[K, V\]\) [Keys]() ```go -// Free frees the linked list. -func (ll *LinkedList[T]) Free() +func (hm *Hashmap[K, V]) Keys() []K ``` -## Benchmarks +Keys returns all keys as a slice -Check the test files and github actions for the benchmarks (linux, macos, windows). -mm-go can sometimes be 5-10 times faster. + +### func \(\*Hashmap\[K, V\]\) [Set]() +```go +func (hm *Hashmap[K, V]) Set(key K, value V) +``` + +Set inserts a new value V if key K doesn't exist, Otherwise update the key K with value V + + +### func \(\*Hashmap\[K, V\]\) [Values]() + +```go +func (hm *Hashmap[K, V]) Values() []V ``` -Run go test ./... -bench=. -count 5 > out.txt && benchstat out.txt -name time/op -pkg:github.com/joetifa2003/mm-go goos:linux goarch:amd64 -HeapManaged/node_count_10000-2 504µs ± 1% -HeapManaged/node_count_100000-2 3.73ms ± 6% -HeapManaged/node_count_10000000-2 664ms ± 8% -HeapManaged/node_count_100000000-2 6.30s ± 4% -Manual/node_count_10000-2 226µs ± 1% -Manual/node_count_100000-2 576µs ± 1% -Manual/node_count_10000000-2 70.6ms ± 1% -Manual/node_count_100000000-2 702ms ± 1% -ArenaManual/node_count_10000-2 226µs ± 1% -ArenaManual/node_count_100000-2 553µs ± 0% -ArenaManual/node_count_10000000-2 69.1ms ± 0% -ArenaManual/node_count_100000000-2 681ms ± 1% -BinaryTreeManaged-2 6.07s ±10% -BinaryTreeArena/chunk_size_50-2 2.30s ±21% -BinaryTreeArena/chunk_size_100-2 1.47s ± 5% -BinaryTreeArena/chunk_size_150-2 1.42s ±36% -BinaryTreeArena/chunk_size_250-2 1.11s ± 0% -BinaryTreeArena/chunk_size_500-2 1.00s ± 0% +Values returns all values as a slice + +# linkedlist + +```go +import "github.com/joetifa2003/mm-go/linkedlist" ``` + +
Example +

+ + + +```go +alloc := allocator.NewC() +defer alloc.Destroy() + +ll := New[int](alloc) +defer ll.Free() + +ll.PushBack(1) +ll.PushBack(2) +ll.PushBack(3) +ll.PushBack(4) + +fmt.Println("PopBack:", ll.PopBack()) +fmt.Println("PopFront:", ll.PopFront()) + +for _, i := range ll.Iter() { + fmt.Println(i) +} + +// Output: +// PopBack: 4 +// PopFront: 1 +// 2 +// 3 +``` + +#### Output + +``` +PopBack: 4 +PopFront: 1 +2 +3 +``` + +

+
+ +## Index + +- [type LinkedList](<#LinkedList>) + - [func New\[T any\]\(alloc allocator.Allocator\) \*LinkedList\[T\]](<#New>) + - [func \(ll \*LinkedList\[T\]\) At\(idx int\) T](<#LinkedList[T].At>) + - [func \(ll \*LinkedList\[T\]\) AtPtr\(idx int\) \*T](<#LinkedList[T].AtPtr>) + - [func \(ll \*LinkedList\[T\]\) FindIndex\(f func\(value T\) bool\) \(idx int, ok bool\)](<#LinkedList[T].FindIndex>) + - [func \(ll \*LinkedList\[T\]\) FindIndexes\(f func\(value T\) bool\) \[\]int](<#LinkedList[T].FindIndexes>) + - [func \(ll \*LinkedList\[T\]\) ForEach\(f func\(idx int, value T\)\)](<#LinkedList[T].ForEach>) + - [func \(ll \*LinkedList\[T\]\) Free\(\)](<#LinkedList[T].Free>) + - [func \(ll \*LinkedList\[T\]\) Iter\(\) iter.Seq2\[int, T\]](<#LinkedList[T].Iter>) + - [func \(ll \*LinkedList\[T\]\) Len\(\) int](<#LinkedList[T].Len>) + - [func \(ll \*LinkedList\[T\]\) PopBack\(\) T](<#LinkedList[T].PopBack>) + - [func \(ll \*LinkedList\[T\]\) PopFront\(\) T](<#LinkedList[T].PopFront>) + - [func \(ll \*LinkedList\[T\]\) PushBack\(value T\)](<#LinkedList[T].PushBack>) + - [func \(ll \*LinkedList\[T\]\) PushFront\(value T\)](<#LinkedList[T].PushFront>) + - [func \(ll \*LinkedList\[T\]\) Remove\(f func\(idx int, value T\) bool\) \(value T, ok bool\)](<#LinkedList[T].Remove>) + - [func \(ll \*LinkedList\[T\]\) RemoveAll\(f func\(idx int, value T\) bool\) \[\]T](<#LinkedList[T].RemoveAll>) + - [func \(ll \*LinkedList\[T\]\) RemoveAt\(idx int\) T](<#LinkedList[T].RemoveAt>) + + + +## type [LinkedList]() + +LinkedList a doubly\-linked list. Note: can be a lot slower than Vector but sometimes faster in specific use cases + +```go +type LinkedList[T any] struct { + // contains filtered or unexported fields +} +``` + + +### func [New]() + +```go +func New[T any](alloc allocator.Allocator) *LinkedList[T] +``` + +New creates a new linked list. + + +### func \(\*LinkedList\[T\]\) [At]() + +```go +func (ll *LinkedList[T]) At(idx int) T +``` + +At gets value T at idx. + + +### func \(\*LinkedList\[T\]\) [AtPtr]() + +```go +func (ll *LinkedList[T]) AtPtr(idx int) *T +``` + +AtPtr gets a pointer to value T at idx. + + +### func \(\*LinkedList\[T\]\) [FindIndex]() + +```go +func (ll *LinkedList[T]) FindIndex(f func(value T) bool) (idx int, ok bool) +``` + +FindIndex returns the first index of value T that pass the test implemented by the provided function. + + +### func \(\*LinkedList\[T\]\) [FindIndexes]() + +```go +func (ll *LinkedList[T]) FindIndexes(f func(value T) bool) []int +``` + +FindIndex returns all indexes of value T that pass the test implemented by the provided function. + + +### func \(\*LinkedList\[T\]\) [ForEach]() + +```go +func (ll *LinkedList[T]) ForEach(f func(idx int, value T)) +``` + +ForEach iterates through the linked list. + + +### func \(\*LinkedList\[T\]\) [Free]() + +```go +func (ll *LinkedList[T]) Free() +``` + +Free frees the linked list. + + +### func \(\*LinkedList\[T\]\) [Iter]() + +```go +func (ll *LinkedList[T]) Iter() iter.Seq2[int, T] +``` + +Iter returns an iterator over the linked list values. + + +### func \(\*LinkedList\[T\]\) [Len]() + +```go +func (ll *LinkedList[T]) Len() int +``` + +Len gets linked list length. + + +### func \(\*LinkedList\[T\]\) [PopBack]() + +```go +func (ll *LinkedList[T]) PopBack() T +``` + +PopBack pops and returns value T from the back of the linked list. + + +### func \(\*LinkedList\[T\]\) [PopFront]() + +```go +func (ll *LinkedList[T]) PopFront() T +``` + +PopFront pops and returns value T from the front of the linked list. + + +### func \(\*LinkedList\[T\]\) [PushBack]() + +```go +func (ll *LinkedList[T]) PushBack(value T) +``` + +PushBack pushes value T to the back of the linked list. + + +### func \(\*LinkedList\[T\]\) [PushFront]() + +```go +func (ll *LinkedList[T]) PushFront(value T) +``` + +PushFront pushes value T to the back of the linked list. + + +### func \(\*LinkedList\[T\]\) [Remove]() + +```go +func (ll *LinkedList[T]) Remove(f func(idx int, value T) bool) (value T, ok bool) +``` + +Remove removes the first value T that pass the test implemented by the provided function. if the test succeeded it will return the value and true + + +### func \(\*LinkedList\[T\]\) [RemoveAll]() + +```go +func (ll *LinkedList[T]) RemoveAll(f func(idx int, value T) bool) []T +``` + +RemoveAll removes all values of T that pass the test implemented by the provided function. + + +### func \(\*LinkedList\[T\]\) [RemoveAt]() + +```go +func (ll *LinkedList[T]) RemoveAt(idx int) T +``` + +RemoveAt removes value T at specified index and returns it. + +# minheap + +```go +import "github.com/joetifa2003/mm-go/minheap" +``` + +
Example +

+ + + +```go +package main + +import ( + "fmt" + + "github.com/joetifa2003/mm-go/allocator" + "github.com/joetifa2003/mm-go/minheap" +) + +func int_less(a, b int) bool { return a < b } + +func main() { + alloc := allocator.NewC() + defer alloc.Destroy() + + h := minheap.New[int](alloc, int_less) + + // Push some values onto the heap + h.Push(2) + h.Push(1) + h.Push(4) + h.Push(3) + h.Push(5) + + // Pop the minimum value from the heap + fmt.Println(h.Pop()) + fmt.Println(h.Pop()) + +} +``` + +#### Output + +``` +1 +2 +``` + +

+
+ +
Example (-ax Heap) +

+ + + +```go +package main + +import ( + "fmt" + + "github.com/joetifa2003/mm-go/allocator" + "github.com/joetifa2003/mm-go/minheap" +) + +func int_greater(a, b int) bool { return a > b } + +func main() { + alloc := allocator.NewC() + defer alloc.Destroy() + + h := minheap.New[int](alloc, int_greater) + + // Push some values onto the heap + h.Push(2) + h.Push(1) + h.Push(4) + h.Push(3) + h.Push(5) + + // Pop the max value from the heap + fmt.Println(h.Pop()) + fmt.Println(h.Pop()) + +} +``` + +#### Output + +``` +5 +4 +``` + +

+
+ +## Index + +- [type MinHeap](<#MinHeap>) + - [func New\[T any\]\(alloc allocator.Allocator, less func\(a, b T\) bool\) \*MinHeap\[T\]](<#New>) + - [func \(h \*MinHeap\[T\]\) Free\(\)](<#MinHeap[T].Free>) + - [func \(h \*MinHeap\[T\]\) Iter\(\) iter.Seq2\[int, T\]](<#MinHeap[T].Iter>) + - [func \(h \*MinHeap\[T\]\) Len\(\) int](<#MinHeap[T].Len>) + - [func \(h \*MinHeap\[T\]\) Peek\(\) T](<#MinHeap[T].Peek>) + - [func \(h \*MinHeap\[T\]\) Pop\(\) T](<#MinHeap[T].Pop>) + - [func \(h \*MinHeap\[T\]\) Push\(value T\)](<#MinHeap[T].Push>) + - [func \(h \*MinHeap\[T\]\) Remove\(f func\(T\) bool\)](<#MinHeap[T].Remove>) + + + +## type [MinHeap]() + + + +```go +type MinHeap[T any] struct { + // contains filtered or unexported fields +} +``` + + +### func [New]() + +```go +func New[T any](alloc allocator.Allocator, less func(a, b T) bool) *MinHeap[T] +``` + +New creates a new MinHeap. + + +### func \(\*MinHeap\[T\]\) [Free]() + +```go +func (h *MinHeap[T]) Free() +``` + +Free frees the heap. + + +### func \(\*MinHeap\[T\]\) [Iter]() + +```go +func (h *MinHeap[T]) Iter() iter.Seq2[int, T] +``` + +Iter returns an iterator over the elements of the heap. + + +### func \(\*MinHeap\[T\]\) [Len]() + +```go +func (h *MinHeap[T]) Len() int +``` + +Len returns the number of elements in the heap. + + +### func \(\*MinHeap\[T\]\) [Peek]() + +```go +func (h *MinHeap[T]) Peek() T +``` + +Peek returns the minimum value from the heap without removing it. + + +### func \(\*MinHeap\[T\]\) [Pop]() + +```go +func (h *MinHeap[T]) Pop() T +``` + +Pop removes and returns the minimum value from the heap. + + +### func \(\*MinHeap\[T\]\) [Push]() + +```go +func (h *MinHeap[T]) Push(value T) +``` + +Push adds a value to the heap. + + +### func \(\*MinHeap\[T\]\) [Remove]() + +```go +func (h *MinHeap[T]) Remove(f func(T) bool) +``` + +Remove the first element that makes f return true + +# mmstring + +```go +import "github.com/joetifa2003/mm-go/mmstring" +``` + +
Example +

+ + + +```go +package main + +import ( + "fmt" + + "github.com/joetifa2003/mm-go/allocator" + "github.com/joetifa2003/mm-go/mmstring" +) + +func main() { + alloc := allocator.NewC() + defer alloc.Destroy() + + s := mmstring.New(alloc) + defer s.Free() + + s.AppendGoString("Hello ") + s.AppendGoString("World") + + s2 := mmstring.From(alloc, "Foo Bar") + defer s2.Free() + + fmt.Println(s.GetGoString()) + fmt.Println(s2.GetGoString()) + +} +``` + +#### Output + +``` +Hello World +Foo Bar +``` + +

+
+ +
Example (Datastructures) +

+ + + +```go +package main + +import ( + "fmt" + + "github.com/joetifa2003/mm-go/allocator" + "github.com/joetifa2003/mm-go/batchallocator" + "github.com/joetifa2003/mm-go/mmstring" + "github.com/joetifa2003/mm-go/vector" +) + +func main() { + alloc := batchallocator.New(allocator.NewC()) + defer alloc.Destroy() // all the memory allocated bellow will be freed, no need to free it manually. + + m := vector.New[*mmstring.MMString](alloc) + m.Push(mmstring.From(alloc, "hello")) + m.Push(mmstring.From(alloc, "world")) + + for k, v := range m.Iter() { + fmt.Println(k, v.GetGoString()) + } + +} +``` + +#### Output + +``` +0 hello +1 world +``` + +

+
+ +## Index + +- [type MMString](<#MMString>) + - [func From\(alloc allocator.Allocator, input string\) \*MMString](<#From>) + - [func New\(alloc allocator.Allocator\) \*MMString](<#New>) + - [func \(s \*MMString\) AppendGoString\(input string\)](<#MMString.AppendGoString>) + - [func \(s \*MMString\) Free\(\)](<#MMString.Free>) + - [func \(s \*MMString\) GetGoString\(\) string](<#MMString.GetGoString>) + + + +## type [MMString]() + +MMString is a manually manged string that is basically a \*Vector\[rune\] and contains all the methods of a vector plus additional helper functions + +```go +type MMString struct { + // contains filtered or unexported fields +} +``` + + +### func [From]() + +```go +func From(alloc allocator.Allocator, input string) *MMString +``` + +From creates a new manually managed string, And initialize it with a go string + + +### func [New]() + +```go +func New(alloc allocator.Allocator) *MMString +``` + +New create a new manually managed string + + +### func \(\*MMString\) [AppendGoString]() + +```go +func (s *MMString) AppendGoString(input string) +``` + +AppendGoString appends go string to manually managed string + + +### func \(\*MMString\) [Free]() + +```go +func (s *MMString) Free() +``` + +Free frees MMString + + +### func \(\*MMString\) [GetGoString]() + +```go +func (s *MMString) GetGoString() string +``` + +GetGoString returns go string from manually managed string. CAUTION: You also have to free the MMString + +# typedarena + +```go +import "github.com/joetifa2003/mm-go/typedarena" +``` + +typedarena is a growable typed arena that allocates memory in fixed chunks , it's faster that batchallocator but more limited, you can use batchallocator if you want to allocate multiple different types, and you want to use an arena like behavior spanning multiple datastructures \(like vector, linkedlist, hashmap etc..\), typedarena is much faster when you are only allocating one type. + +
Example +

+ + + +```go +package main + +import ( + "fmt" + + "github.com/joetifa2003/mm-go/allocator" + "github.com/joetifa2003/mm-go/typedarena" +) + +type Entity struct { + VelocityX float32 + VelocityY float32 + PositionX float32 + PositionY float32 +} + +func main() { + alloc := allocator.NewC() + defer alloc.Destroy() + + arena := typedarena.New[Entity]( + alloc, + 10, + ) + defer arena.Free() // frees all memory + + for i := 0; i < 10; i++ { + e := arena.Alloc() // *Entity + e.VelocityX = float32(i) + e.VelocityY = float32(i) + e.PositionX = float32(i) + e.PositionY = float32(i) + fmt.Println(e.VelocityX, e.VelocityY, e.PositionX, e.PositionY) + } + + entities := arena.AllocMany(10) // allocate slice of 10 entities (cannot exceed 10 here because chunk size is 10 above, this limitation doesn't exist in batchallocator) + + _ = entities + +} +``` + +#### Output + +``` +0 0 0 0 +1 1 1 1 +2 2 2 2 +3 3 3 3 +4 4 4 4 +5 5 5 5 +6 6 6 6 +7 7 7 7 +8 8 8 8 +9 9 9 9 +``` + +

+
+ +## Index + +- [type TypedArena](<#TypedArena>) + - [func New\[T any\]\(alloc allocator.Allocator, chunkSize int\) \*TypedArena\[T\]](<#New>) + - [func \(ta \*TypedArena\[T\]\) Alloc\(\) \*T](<#TypedArena[T].Alloc>) + - [func \(ta \*TypedArena\[T\]\) AllocMany\(n int\) \[\]T](<#TypedArena[T].AllocMany>) + - [func \(ta \*TypedArena\[T\]\) Free\(\)](<#TypedArena[T].Free>) + + + +## type [TypedArena]() + +TypedArena is a growable typed arena + +```go +type TypedArena[T any] struct { + // contains filtered or unexported fields +} +``` + + +### func [New]() + +```go +func New[T any](alloc allocator.Allocator, chunkSize int) *TypedArena[T] +``` + +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 + + +### func \(\*TypedArena\[T\]\) [Alloc]() + +```go +func (ta *TypedArena[T]) Alloc() *T +``` + +Alloc allocates T from the arena + + +### func \(\*TypedArena\[T\]\) [AllocMany]() + +```go +func (ta *TypedArena[T]) AllocMany(n int) []T +``` + +AllocMany allocates n of T and returns a slice representing the heap. CAUTION: don't append to the slice, the purpose of it is to replace pointer arithmetic with slice indexing CAUTION: n cannot exceed chunk size + + +### func \(\*TypedArena\[T\]\) [Free]() + +```go +func (ta *TypedArena[T]) Free() +``` + +Free frees all allocated memory + +# vector + +```go +import "github.com/joetifa2003/mm-go/vector" +``` + +
Example +

+ + + +```go +package main + +import ( + "fmt" + + "github.com/joetifa2003/mm-go/allocator" + "github.com/joetifa2003/mm-go/vector" +) + +func main() { + alloc := allocator.NewC() + v := vector.New[int](alloc) + v.Push(1) + v.Push(2) + v.Push(3) + + fmt.Println("Length:", v.Len()) + for i := 0; i < v.Len(); i++ { + fmt.Println(v.At(i)) + } + + for _, k := range v.Iter() { + fmt.Println(k) + } + +} +``` + +#### Output + +``` +Length: 3 +1 +2 +3 +1 +2 +3 +``` + +

+
+ +## Index + +- [type Vector](<#Vector>) + - [func Init\[T any\]\(alloc allocator.Allocator, values ...T\) \*Vector\[T\]](<#Init>) + - [func New\[T any\]\(aloc allocator.Allocator, args ...int\) \*Vector\[T\]](<#New>) + - [func \(v \*Vector\[T\]\) At\(idx int\) T](<#Vector[T].At>) + - [func \(v \*Vector\[T\]\) AtPtr\(idx int\) \*T](<#Vector[T].AtPtr>) + - [func \(v \*Vector\[T\]\) Cap\(\) int](<#Vector[T].Cap>) + - [func \(v \*Vector\[T\]\) Free\(\)](<#Vector[T].Free>) + - [func \(v \*Vector\[T\]\) Iter\(\) iter.Seq2\[int, T\]](<#Vector[T].Iter>) + - [func \(v \*Vector\[T\]\) Last\(\) T](<#Vector[T].Last>) + - [func \(v \*Vector\[T\]\) Len\(\) int](<#Vector[T].Len>) + - [func \(v \*Vector\[T\]\) Pop\(\) T](<#Vector[T].Pop>) + - [func \(v \*Vector\[T\]\) Push\(value T\)](<#Vector[T].Push>) + - [func \(v \*Vector\[T\]\) RemoveAt\(idx int\) T](<#Vector[T].RemoveAt>) + - [func \(v \*Vector\[T\]\) Set\(idx int, value T\)](<#Vector[T].Set>) + - [func \(v \*Vector\[T\]\) Slice\(\) \[\]T](<#Vector[T].Slice>) + - [func \(v \*Vector\[T\]\) UnsafeAt\(idx int\) T](<#Vector[T].UnsafeAt>) + + + +## type [Vector]() + +Vector a contiguous growable array type + +```go +type Vector[T any] struct { + // contains filtered or unexported fields +} +``` + + +### func [Init]() + +```go +func Init[T any](alloc allocator.Allocator, values ...T) *Vector[T] +``` + +Init initializes a new vector with the T elements provided and sets it's len and cap to len\(values\) + + +### func [New]() + +```go +func New[T any](aloc allocator.Allocator, args ...int) *Vector[T] +``` + +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 \(\*Vector\[T\]\) [At]() + +```go +func (v *Vector[T]) At(idx int) T +``` + +At gets element T at specified index + + +### func \(\*Vector\[T\]\) [AtPtr]() + +```go +func (v *Vector[T]) AtPtr(idx int) *T +``` + +AtPtr gets element a pointer of T at specified index + + +### func \(\*Vector\[T\]\) [Cap]() + +```go +func (v *Vector[T]) Cap() int +``` + +Cap gets vector capacity \(underling memory length\). + + +### func \(\*Vector\[T\]\) [Free]() + +```go +func (v *Vector[T]) Free() +``` + +Free deallocats the vector + + +### func \(\*Vector\[T\]\) [Iter]() + +```go +func (v *Vector[T]) Iter() iter.Seq2[int, T] +``` + +Iter iterates over the vector + + +### func \(\*Vector\[T\]\) [Last]() + +```go +func (v *Vector[T]) Last() T +``` + +Last gets the last element from a vector + + +### func \(\*Vector\[T\]\) [Len]() + +```go +func (v *Vector[T]) Len() int +``` + +Len gets vector length + + +### func \(\*Vector\[T\]\) [Pop]() + +```go +func (v *Vector[T]) Pop() T +``` + +Pop pops value T from the vector and returns it + + +### func \(\*Vector\[T\]\) [Push]() + +```go +func (v *Vector[T]) Push(value T) +``` + +Push pushes value T to the vector, grows if needed. + + +### func \(\*Vector\[T\]\) [RemoveAt]() + +```go +func (v *Vector[T]) RemoveAt(idx int) T +``` + + + + +### func \(\*Vector\[T\]\) [Set]() + +```go +func (v *Vector[T]) Set(idx int, value T) +``` + +Set sets element T at specified index + + +### func \(\*Vector\[T\]\) [Slice]() + +```go +func (v *Vector[T]) Slice() []T +``` + +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 \(\*Vector\[T\]\) [UnsafeAt]() + +```go +func (v *Vector[T]) UnsafeAt(idx int) T +``` + +UnsafeAT gets element T at specified index without bounds checking + +Generated by [gomarkdoc]() + + + + + diff --git a/allocator/allocator.go b/allocator/allocator.go new file mode 100644 index 0000000..9914a69 --- /dev/null +++ b/allocator/allocator.go @@ -0,0 +1,95 @@ +package allocator + +import "unsafe" + +// Allocator is an interface that defines some methods needed for most allocators. +// It's not a golang interface, so it's safe to use in manually managed structs (will not get garbage collected). +type Allocator struct { + allocator unsafe.Pointer + alloc func(allocator unsafe.Pointer, size int) unsafe.Pointer + free func(allocator unsafe.Pointer, ptr unsafe.Pointer) + realloc func(allocator unsafe.Pointer, ptr unsafe.Pointer, size int) unsafe.Pointer + destroy func(allocator unsafe.Pointer) +} + +// NewAllocator creates a new Allocator +func NewAllocator( + allocator unsafe.Pointer, + alloc func(allocator unsafe.Pointer, size int) unsafe.Pointer, + free func(allocator unsafe.Pointer, ptr unsafe.Pointer), + realloc func(allocator unsafe.Pointer, ptr unsafe.Pointer, size int) unsafe.Pointer, + destroy func(allocator unsafe.Pointer), +) Allocator { + return Allocator{ + allocator: allocator, + alloc: alloc, + free: free, + realloc: realloc, + destroy: destroy, + } +} + +// Alloc allocates size bytes and returns an unsafe pointer to it. +func (a Allocator) Alloc(size int) unsafe.Pointer { + return a.alloc(a.allocator, size) +} + +// Free frees the memory pointed by ptr +func (a Allocator) Free(ptr unsafe.Pointer) { + a.free(a.allocator, ptr) +} + +// Realloc reallocates the memory pointed by ptr with a new size and returns a new pointer to it. +func (a Allocator) Realloc(ptr unsafe.Pointer, size int) unsafe.Pointer { + return a.realloc(a.allocator, ptr, size) +} + +// Destroy destroys the allocator. +// After calling this, the allocator is no longer usable. +// This is useful for cleanup, freeing allocator internal resources, etc. +func (a Allocator) Destroy() { + a.destroy(a.allocator) +} + +func getSize[T any]() int { + var zeroV T + return int(unsafe.Sizeof(zeroV)) +} + +// Alloc allocates T and returns a pointer to it. +func Alloc[T any](a Allocator) *T { + ptr := a.alloc(a.allocator, getSize[T]()) + return (*T)(unsafe.Pointer(ptr)) +} + +// FreeMany frees memory allocated by Alloc takes a ptr +// CAUTION: be careful not to double free, and prefer using defer to deallocate +func Free[T any](a Allocator, ptr *T) { + a.free(a.allocator, unsafe.Pointer(ptr)) +} + +// AllocMany allocates n of T and returns a slice representing the heap. +// CAUTION: don't append to the slice, the purpose of it is to replace pointer +// arithmetic with slice indexing +func AllocMany[T any](a Allocator, n int) []T { + ptr := a.alloc(a.allocator, getSize[T]()*n) + return unsafe.Slice( + (*T)(ptr), + n, + ) +} + +// FreeMany frees memory allocated by AllocMany takes in the slice (aka the heap) +// CAUTION: be careful not to double free, and prefer using defer to deallocate +func FreeMany[T any](a Allocator, slice []T) { + a.free(a.allocator, unsafe.Pointer(&slice[0])) +} + +// Realloc reallocates memory allocated with AllocMany and doesn't change underling data +func Realloc[T any](a Allocator, slice []T, newN int) []T { + ptr := a.realloc(a.allocator, unsafe.Pointer(&slice[0]), getSize[T]()*newN) + return unsafe.Slice( + (*T)(ptr), + newN, + ) +} diff --git a/allocator/callocator.go b/allocator/callocator.go new file mode 100644 index 0000000..516cf75 --- /dev/null +++ b/allocator/callocator.go @@ -0,0 +1,25 @@ +package allocator + +// #include +import "C" + +import "unsafe" + +// NewC returns an allocator that uses C calloc, realloc and free. +func NewC() Allocator { + return NewAllocator(nil, callocator_alloc, callocator_free, callocator_realloc, callocator_destroy) +} + +func callocator_alloc(allocator unsafe.Pointer, size int) unsafe.Pointer { + return C.calloc(1, C.size_t(size)) +} + +func callocator_free(allocator unsafe.Pointer, ptr unsafe.Pointer) { + C.free(ptr) +} + +func callocator_realloc(allocator unsafe.Pointer, ptr unsafe.Pointer, size int) unsafe.Pointer { + return C.realloc(ptr, C.size_t(size)) +} + +func callocator_destroy(allocator unsafe.Pointer) {} diff --git a/allocator/example_test.go b/allocator/example_test.go new file mode 100644 index 0000000..2898955 --- /dev/null +++ b/allocator/example_test.go @@ -0,0 +1,165 @@ +package allocator_test + +import ( + "fmt" + "unsafe" + + "github.com/joetifa2003/mm-go" + "github.com/joetifa2003/mm-go/allocator" + "github.com/joetifa2003/mm-go/linkedlist" + "github.com/joetifa2003/mm-go/mmstring" + "github.com/joetifa2003/mm-go/vector" +) + +func Example() { + alloc := allocator.NewC() + defer alloc.Destroy() + + ptr := allocator.Alloc[int](alloc) + defer allocator.Free(alloc, ptr) + + *ptr = 15 + fmt.Println(*ptr) + + // Output: 15 +} + +type MyStruct struct { + a int + b float32 +} + +func Example_datastructures() { + alloc := allocator.NewC() + defer alloc.Destroy() + + p := allocator.Alloc[MyStruct](alloc) + defer allocator.Free(alloc, p) + + p.a = 100 + p.b = 200 + + fmt.Println(*p) + + v := vector.New[int](alloc) + defer v.Free() + v.Push(15) + v.Push(70) + + for _, i := range v.Iter() { + fmt.Println(i) + } + + l := linkedlist.New[*mmstring.MMString](alloc) + defer l.Free() + l.PushBack(mmstring.From(alloc, "hello")) + l.PushBack(mmstring.From(alloc, "world")) + + for _, i := range l.Iter() { + fmt.Println(i.GetGoString()) + } + + // Output: + // {100 200} + // 15 + // 70 + // hello + // world +} + +func ExampleAlloc() { + alloc := allocator.NewC() + defer alloc.Destroy() + + // So you can do this: + 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) + *ptr = 15 + fmt.Println(*ptr) + + // instead of doing this: + ptr2 := (*int)(alloc.Alloc(mm.SizeOf[int]())) + defer alloc.Free(unsafe.Pointer(ptr2)) + *ptr2 = 15 + + fmt.Println(*ptr2) + + // Output: + // 15 + // 15 +} + +func ExampleAllocMany() { + alloc := allocator.NewC() + defer alloc.Destroy() + + heap := allocator.AllocMany[int](alloc, 2) // allocates 2 ints and returns it as a slice of ints with length 2 + defer allocator.FreeMany(alloc, heap) // it's recommended to make sure the data gets deallocated (defer recommended to prevent leaks) + + 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 + heap[1] = 70 + + fmt.Println(heap[0]) + fmt.Println(heap[1]) + + // Output: + // 45 + // 70 +} + +func ExampleRealloc() { + alloc := allocator.NewC() + defer alloc.Destroy() + + heap := allocator.AllocMany[int](alloc, 2) // allocates 2 int and returns it as a slice of ints with length 2 + + heap[0] = 15 + heap[1] = 70 + + heap = allocator.Realloc(alloc, heap, 3) + heap[2] = 100 + + fmt.Println(heap[0]) + fmt.Println(heap[1]) + fmt.Println(heap[2]) + + allocator.FreeMany(alloc, heap) + + // Output: + // 15 + // 70 + // 100 +} + +func ExampleNewAllocator() { + // Create a custom allocator + alloc := allocator.NewAllocator( + nil, + myallocator_alloc, + myallocator_free, + myallocator_realloc, + myallocator_destroy, + ) + + // Check how C allocator is implemented + // or batchallocator source for a reference + + _ = alloc +} + +func myallocator_alloc(allocator unsafe.Pointer, size int) unsafe.Pointer { + return nil +} + +func myallocator_free(allocator unsafe.Pointer, ptr unsafe.Pointer) { +} + +func myallocator_realloc(allocator unsafe.Pointer, ptr unsafe.Pointer, size int) unsafe.Pointer { + return nil +} + +func myallocator_destroy(allocator unsafe.Pointer) { +} diff --git a/batchallocator/batch.go b/batchallocator/batch.go new file mode 100644 index 0000000..b8239dd --- /dev/null +++ b/batchallocator/batch.go @@ -0,0 +1,202 @@ +// 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. +// It allocats large chunks of memory at once and then divides them when you allocate, making it much faster. +// This allocator has to take another allocator for it to work, usually with the C allocator. +// You can optionally call `Free` on the pointers allocated by batchallocator manually, and it will free the memory as soon as it can. +// `Destroy` must be called to free internal resources and free all the memory allocated by the allocator. +package batchallocator + +import ( + "os" + "unsafe" + + "github.com/joetifa2003/mm-go/allocator" + "github.com/joetifa2003/mm-go/minheap" +) + +var pageSize = os.Getpagesize() + +const ( + alignment = unsafe.Alignof(uintptr(0)) + sizeOfPtrMeta = unsafe.Sizeof(ptrMeta{}) // Cache the size of ptrMeta type +) + +type bucket struct { + data unsafe.Pointer // Base pointer of the bucket memory + offset uintptr // Number of used bytes in this bucket + size uintptr // Total size of this bucket + ptrs int // Number of pointers (allocations) inside the bucket +} + +func (b *bucket) Free(a allocator.Allocator) { + a.Free(b.data) + a.Free(unsafe.Pointer(b)) +} + +// A metadata structure stored before each allocated memory block +type ptrMeta struct { + bucket *bucket // Pointer back to the bucket + size int // Size of the allocated memory block +} + +// BatchAllocator manages a collection of memory buckets to optimize small allocations +type BatchAllocator struct { + buckets *minheap.MinHeap[*bucket] // Min-heap for managing buckets based on used space + alloc allocator.Allocator // Underlying raw allocator (backed by malloc/free) + bucketSize int // Configurable size for each new bucket +} + +type BatchAllocatorOption func(alloc *BatchAllocator) + +// WithBucketSize Option to specify bucket size when creating BatchAllocator +// You can allocate more memory than the bucketsize in one allocation, it will allocate a new bucket and put the data in it. +func WithBucketSize(size int) BatchAllocatorOption { + return func(alloc *BatchAllocator) { + alloc.bucketSize = size + } +} + +// New creates a new BatchAllocator and applies optional configuration using BatchAllocatorOption +func New(a allocator.Allocator, options ...BatchAllocatorOption) allocator.Allocator { + balloc := allocator.Alloc[BatchAllocator](a) + balloc.alloc = a + + // Apply configuration options to BatchAllocator + for _, option := range options { + option(balloc) + } + + return allocator.NewAllocator( + unsafe.Pointer(balloc), + batchAllocatorAlloc, + batchAllocatorFree, + batchAllocatorRealloc, + batchAllocatorDestroy, + ) +} + +// Performs the allocation from the BatchAllocator +func batchAllocatorAlloc(allocator unsafe.Pointer, size int) unsafe.Pointer { + balloc := (*BatchAllocator)(allocator) + + // Ensure we have a bucket to allocate from + ensureBucketExists(balloc, size) + + // Check if the current top bucket can handle the allocation + currentBucket := balloc.buckets.Peek() + + if currentBucket.offset+sizeOfPtrMeta+uintptr(size) <= currentBucket.size { + currentBucket = balloc.buckets.Pop() + + freeStart := uintptr(currentBucket.data) + currentBucket.offset + + // Write the allocation metadata + meta := (*ptrMeta)(unsafe.Pointer(freeStart)) + meta.bucket = currentBucket + meta.size = size + + currentBucket.offset = align(currentBucket.offset+sizeOfPtrMeta+uintptr(size), alignment) + currentBucket.ptrs++ + + balloc.buckets.Push(meta.bucket) + + // Return the address of the memory after the metadata + return unsafe.Pointer(freeStart + uintptr(sizeOfPtrMeta)) + } + + // If no bucket can accommodate the allocation, create a new one + newBucket := allocateNewBucket(balloc, size) + newBucket.offset = align(sizeOfPtrMeta+uintptr(size), alignment) + newBucket.ptrs++ + balloc.buckets.Push(newBucket) + + // Write meta information at the base of the new bucket + meta := (*ptrMeta)(unsafe.Pointer(newBucket.data)) + meta.bucket = newBucket + meta.size = size + + return unsafe.Pointer(uintptr(unsafe.Pointer(meta)) + uintptr(sizeOfPtrMeta)) +} + +// Frees the allocated memory by decrementing reference count and freeing bucket if empty +func batchAllocatorFree(allocator unsafe.Pointer, ptr unsafe.Pointer) { + balloc := (*BatchAllocator)(allocator) + + // Retrieve the metadata by moving back + meta := (*ptrMeta)(unsafe.Pointer(uintptr(ptr) - sizeOfPtrMeta)) + meta.bucket.ptrs-- + + // If no more pointers exist in the bucket, free the bucket + if meta.bucket.ptrs == 0 { + balloc.buckets.Remove(func(b *bucket) bool { + return b == meta.bucket + }) + meta.bucket.Free(balloc.alloc) + } +} + +// Reallocate a block of memory +func batchAllocatorRealloc(allocator unsafe.Pointer, ptr unsafe.Pointer, size int) unsafe.Pointer { + newPtr := batchAllocatorAlloc(allocator, size) + + // Copy the data from the old location to the new one + oldMeta := (*ptrMeta)(unsafe.Pointer(uintptr(ptr) - sizeOfPtrMeta)) + oldData := unsafe.Slice((*byte)(ptr), oldMeta.size) + newData := unsafe.Slice((*byte)(newPtr), size) + + copy(newData, oldData) + + // Free the old memory + batchAllocatorFree(allocator, ptr) + + return newPtr +} + +// Destroys the batch allocator, freeing all buckets and underlying library resources +func batchAllocatorDestroy(a unsafe.Pointer) { + balloc := (*BatchAllocator)(a) + + // Free all buckets in the heap + for _, b := range balloc.buckets.Iter() { + b.Free(balloc.alloc) + } + + balloc.buckets.Free() + allocator.Free(balloc.alloc, balloc) +} + +// Helper function to handle memory alignment for a given pointer +func align(ptr uintptr, alignment uintptr) uintptr { + mask := alignment - 1 + return (ptr + mask) &^ mask +} + +// Allocates a new bucket with a given size, ensuring it's a multiple of the page size +func allocateNewBucket(balloc *BatchAllocator, size int) *bucket { + size = max(balloc.bucketSize, size) + + nPages := size/pageSize + 1 + bucketSize := nPages * pageSize + + b := allocator.Alloc[bucket](balloc.alloc) + b.data = balloc.alloc.Alloc(bucketSize) + b.size = uintptr(bucketSize) + b.offset = 0 + + return b +} + +// Pushes a new bucket into the bucket heap if needed +func ensureBucketExists(balloc *BatchAllocator, size int) { + if balloc.buckets == nil { + balloc.buckets = minheap.New(balloc.alloc, compareBucketFreeSpace) + } + + if balloc.buckets.Len() == 0 { + balloc.buckets.Push(allocateNewBucket(balloc, size)) + } +} + +// Comparison function to prioritize buckets with more available space +func compareBucketFreeSpace(a, b *bucket) bool { + return (a.size - a.offset) > (b.size - b.offset) +} diff --git a/batchallocator/batch_test.go b/batchallocator/batch_test.go new file mode 100644 index 0000000..8edcb9c --- /dev/null +++ b/batchallocator/batch_test.go @@ -0,0 +1,52 @@ +package batchallocator + +import ( + "testing" + "unsafe" + + "github.com/stretchr/testify/require" + + "github.com/joetifa2003/mm-go/allocator" +) + +func TestBatchAllocator(t *testing.T) { + assert := require.New(t) + + alloc := New(allocator.NewC()) + + i := allocator.Alloc[int](alloc) + *i = 1 + j := allocator.Alloc[int](alloc) + *j = 2 + + assert.Equal(1, *i) + assert.Equal(2, *j) + + arr := allocator.Alloc[[3000]int](alloc) + for i := 0; i < 32; i++ { + arr[i] = i + } + + allocator.Free(alloc, i) + + assert.Equal(2, *j) + + allocator.Free(alloc, j) + + allocator.Free(alloc, arr) + + alloc.Destroy() +} + +func TestBatchAllocatorAligned(t *testing.T) { + assert := require.New(t) + + alloc := New(allocator.NewC()) + + alloc.Alloc(13) + alloc.Alloc(11) + y := allocator.Alloc[int](alloc) + *y = 2 + + assert.Equal(0, int(uintptr(unsafe.Pointer(y))%8)) +} diff --git a/batchallocator/example_test.go b/batchallocator/example_test.go new file mode 100644 index 0000000..15c8049 --- /dev/null +++ b/batchallocator/example_test.go @@ -0,0 +1,63 @@ +package batchallocator_test + +import ( + "fmt" + + "github.com/joetifa2003/mm-go" + "github.com/joetifa2003/mm-go/allocator" + "github.com/joetifa2003/mm-go/batchallocator" + "github.com/joetifa2003/mm-go/linkedlist" + "github.com/joetifa2003/mm-go/mmstring" + "github.com/joetifa2003/mm-go/vector" +) + +func Example() { + alloc := batchallocator.New(allocator.NewC()) // by default it allocates page, which is usually 4kb + defer alloc.Destroy() // this frees all memory allocated by the allocator automatically + + ptr := allocator.Alloc[int](alloc) + // but you can still free the pointers manually if you want (will free buckets of memory if all pointers depending on it is freed) + defer allocator.Free(alloc, ptr) // this can removed and the memory will be freed. +} + +func Example_arena() { + alloc := batchallocator.New(allocator.NewC()) + defer alloc.Destroy() // all the memory allocated bellow will be freed, no need to free it manually. + + v := vector.New[int](alloc) + v.Push(15) + v.Push(70) + + for _, i := range v.Iter() { + fmt.Println(i) + } + + l := linkedlist.New[*mmstring.MMString](alloc) + l.PushBack(mmstring.From(alloc, "hello")) + l.PushBack(mmstring.From(alloc, "world")) + + for _, i := range l.Iter() { + fmt.Println(i.GetGoString()) + } + + // Output: + // 15 + // 70 + // hello + // world +} + +func ExampleWithBucketSize() { + alloc := batchallocator.New( + allocator.NewC(), + batchallocator.WithBucketSize(mm.SizeOf[int]()*15), // configure the allocator to allocate size of 15 ints per bucket. + ) + defer alloc.Destroy() + + ptr := allocator.Alloc[int](alloc) + defer allocator.Free(alloc, ptr) // this can be removed and the memory will still be freed on Destroy. + + ptr2 := allocator.Alloc[int](alloc) // will not call CGO because there is still enough memory in the Bucket. + defer allocator.Free(alloc, ptr2) // this can be removed and the memory will still be freed on Destroy. + +} diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..7eb0355 --- /dev/null +++ b/example_test.go @@ -0,0 +1,15 @@ +package mm_test + +import ( + "fmt" + + "github.com/joetifa2003/mm-go" +) + +func ExampleSizeOf() { + fmt.Println(mm.SizeOf[int32]()) + fmt.Println(mm.SizeOf[int64]()) + // Output: + // 4 + // 8 +} diff --git a/go.mod b/go.mod index e80caec..3dfb0dd 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,14 @@ module github.com/joetifa2003/mm-go -go 1.20 +go 1.23 require ( - github.com/ebitengine/purego v0.4.0-alpha.4 + github.com/dolthub/maphash v0.1.0 github.com/stretchr/testify v1.8.1 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/sys v0.7.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index a06a6aa..0e9147a 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/ebitengine/purego v0.4.0-alpha.4 h1:Y7yIV06Yo5M2BAdD7EVPhfp6LZ0tEcQo5770OhYUVes= -github.com/ebitengine/purego v0.4.0-alpha.4/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= +github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ= +github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -12,8 +12,6 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/hashmap/example_test.go b/hashmap/example_test.go new file mode 100644 index 0000000..313808d --- /dev/null +++ b/hashmap/example_test.go @@ -0,0 +1,34 @@ +package hashmap + +import ( + "fmt" + + "github.com/joetifa2003/mm-go/allocator" + "github.com/joetifa2003/mm-go/batchallocator" +) + +func Example() { + alloc := batchallocator.New(allocator.NewC()) + defer alloc.Destroy() + + hm := New[int, int](alloc) + defer hm.Free() // can be removed + + hm.Set(1, 10) + hm.Set(2, 20) + hm.Set(3, 30) + + sumKeys := 0 + sumValues := 0 + for k, v := range hm.Iter() { + sumKeys += k + sumValues += v + } + + fmt.Println(sumKeys) + fmt.Println(sumValues) + + // Output: + // 6 + // 60 +} diff --git a/hashmap/hashmap.go b/hashmap/hashmap.go new file mode 100644 index 0000000..53b76a4 --- /dev/null +++ b/hashmap/hashmap.go @@ -0,0 +1,195 @@ +package hashmap + +import ( + "iter" + + "github.com/dolthub/maphash" + + "github.com/joetifa2003/mm-go" + "github.com/joetifa2003/mm-go/allocator" + "github.com/joetifa2003/mm-go/linkedlist" + "github.com/joetifa2003/mm-go/vector" +) + +// Hashmap Manually managed hashmap, +type Hashmap[K comparable, V any] struct { + pairs *vector.Vector[*linkedlist.LinkedList[pair[K, V]]] + totalTaken int + mh maphash.Hasher[K] + alloc allocator.Allocator +} + +type pair[K comparable, V any] struct { + key K + value V +} + +// New creates a new Hashmap with key of type K and value of type V +func New[K comparable, V any](alloc allocator.Allocator) *Hashmap[K, V] { + hm := allocator.Alloc[Hashmap[K, V]](alloc) + hm.pairs = vector.New[*linkedlist.LinkedList[pair[K, V]]](alloc, 8) + hm.mh = maphash.NewHasher[K]() + hm.alloc = alloc + return hm +} + +func (hm *Hashmap[K, V]) extend() { + newPairs := vector.New[*linkedlist.LinkedList[pair[K, V]]](hm.alloc, hm.pairs.Len()*2) + oldPairs := hm.pairs + defer oldPairs.Free() + + hm.totalTaken = 0 + hm.pairs = newPairs + + for _, pairs := range oldPairs.Iter() { + if pairs == nil { + continue + } + + for _, p := range pairs.Iter() { + hm.Set(p.key, p.value) + } + } +} + +// Set inserts a new value V if key K doesn't exist, +// Otherwise update the key K with value V +func (hm *Hashmap[K, V]) Set(key K, value V) { + if ptr, exists := hm.GetPtr(key); exists { + *ptr = value + return + } + + if hm.totalTaken == hm.pairs.Len() { + hm.extend() + } + + hash := hm.mh.Hash(key) + + idx := int(hash % uint64(hm.pairs.Len())) + pairs := hm.pairs.At(idx) + if pairs == nil { + newPairs := linkedlist.New[pair[K, V]](hm.alloc) + hm.pairs.Set(idx, newPairs) + pairs = newPairs + } + pairs.PushBack(pair[K, V]{key: key, value: value}) + hm.totalTaken++ +} + +// Get takes key K and return value V +func (hm *Hashmap[K, V]) Get(key K) (value V, exists bool) { + hash := hm.mh.Hash(key) + + idx := int(hash % uint64(hm.pairs.Len())) + pairs := hm.pairs.At(idx) + if pairs == nil { + return mm.Zero[V](), false + } + + pairIdx, ok := pairs.FindIndex(func(value pair[K, V]) bool { + return value.key == key + }) + if !ok { + return mm.Zero[V](), false + } + + return pairs.At(pairIdx).value, ok +} + +// GetPtr takes key K and return a pointer to value V +func (hm *Hashmap[K, V]) GetPtr(key K) (value *V, exists bool) { + hash := hm.mh.Hash(key) + + idx := int(hash % uint64(hm.pairs.Len())) + pairs := hm.pairs.At(idx) + if pairs == nil { + return nil, false + } + + pairIdx, ok := pairs.FindIndex(func(value pair[K, V]) bool { + return value.key == key + }) + if !ok { + return nil, false + } + + return &pairs.AtPtr(pairIdx).value, ok +} + +// Iter returns an iterator over all key/value pairs +func (hm *Hashmap[K, V]) Iter() iter.Seq2[K, V] { + return func(yield func(K, V) bool) { + for _, pairs := range hm.pairs.Iter() { + if pairs == nil { + continue + } + + for _, pair := range pairs.Iter() { + if !yield(pair.key, pair.value) { + return + } + } + } + } +} + +// Values returns all values as a slice +func (hm *Hashmap[K, V]) Values() []V { + res := make([]V, 0) + + for _, pairs := range hm.pairs.Iter() { + if pairs == nil { + continue + } + + for _, p := range pairs.Iter() { + res = append(res, p.value) + } + } + + return res +} + +// Keys returns all keys as a slice +func (hm *Hashmap[K, V]) Keys() []K { + res := make([]K, 0) + + for _, pairs := range hm.pairs.Iter() { + if pairs == nil { + continue + } + + for _, p := range pairs.Iter() { + res = append(res, p.key) + } + } + + return res +} + +// Delete delete value with key K +func (hm *Hashmap[K, V]) Delete(key K) { + hash := hm.mh.Hash(key) + + idx := int(hash % uint64(hm.pairs.Len())) + pairs := hm.pairs.At(idx) + if pairs == nil { + return + } + + pairs.Remove(func(idx int, p pair[K, V]) bool { + return p.key == key + }) +} + +// Free frees the Hashmap +func (hm *Hashmap[K, V]) Free() { + for _, pairs := range hm.pairs.Iter() { + if pairs != nil { + pairs.Free() + } + } + hm.pairs.Free() + allocator.Free(hm.alloc, hm) +} diff --git a/hashmap/hashmap_test.go b/hashmap/hashmap_test.go new file mode 100644 index 0000000..03e8d9e --- /dev/null +++ b/hashmap/hashmap_test.go @@ -0,0 +1,56 @@ +package hashmap_test + +import ( + "runtime" + "testing" + + "github.com/joetifa2003/mm-go/allocator" + "github.com/joetifa2003/mm-go/batchallocator" + "github.com/joetifa2003/mm-go/hashmap" +) + +const TIMES = 500 + +func BenchmarkHashmapGo(b *testing.B) { + for i := 0; i < b.N; i++ { + h := newMap() + + for i := 0; i < TIMES; i++ { + h[i] = i + } + + runtime.GC() + } +} + +func BenchmarkHashmapCAlloc(b *testing.B) { + for i := 0; i < b.N; i++ { + alloc := allocator.NewC() + h := hashmap.New[int, int](alloc) + + for i := 0; i < TIMES; i++ { + h.Set(i, i) + } + + h.Free() + alloc.Destroy() + } +} + +func BenchmarkHashmapBatchAlloc(b *testing.B) { + for i := 0; i < b.N; i++ { + alloc := batchallocator.New(allocator.NewC()) + h := hashmap.New[int, int](alloc) + + for i := 0; i < TIMES; i++ { + h.Set(i, i) + } + + h.Free() + alloc.Destroy() + } +} + +func newMap() map[int]int { + return make(map[int]int) +} diff --git a/linkedlist/example_test.go b/linkedlist/example_test.go new file mode 100644 index 0000000..7473372 --- /dev/null +++ b/linkedlist/example_test.go @@ -0,0 +1,33 @@ +package linkedlist + +import ( + "fmt" + + "github.com/joetifa2003/mm-go/allocator" +) + +func Example() { + alloc := allocator.NewC() + defer alloc.Destroy() + + ll := New[int](alloc) + defer ll.Free() + + ll.PushBack(1) + ll.PushBack(2) + ll.PushBack(3) + ll.PushBack(4) + + fmt.Println("PopBack:", ll.PopBack()) + fmt.Println("PopFront:", ll.PopFront()) + + for _, i := range ll.Iter() { + fmt.Println(i) + } + + // Output: + // PopBack: 4 + // PopFront: 1 + // 2 + // 3 +} diff --git a/linkedlist/linked_list.go b/linkedlist/linked_list.go index 769ae58..811ec2a 100644 --- a/linkedlist/linked_list.go +++ b/linkedlist/linked_list.go @@ -2,8 +2,9 @@ package linkedlist import ( "fmt" + "iter" - "github.com/joetifa2003/mm-go" + "github.com/joetifa2003/mm-go/allocator" ) var popEmptyMsg = "cannot pop empty linked list" @@ -17,20 +18,23 @@ type linkedListNode[T any] struct { // LinkedList a doubly-linked list. // Note: can be a lot slower than Vector but sometimes faster in specific use cases type LinkedList[T any] struct { + alloc allocator.Allocator + head *linkedListNode[T] tail *linkedListNode[T] length int } // New creates a new linked list. -func New[T any]() *LinkedList[T] { - linkedList := mm.Alloc[LinkedList[T]]() +func New[T any](alloc allocator.Allocator) *LinkedList[T] { + linkedList := allocator.Alloc[LinkedList[T]](alloc) + linkedList.alloc = alloc return linkedList } func (ll *LinkedList[T]) init(value T) { - ll.head = mm.Alloc[linkedListNode[T]]() + ll.head = allocator.Alloc[linkedListNode[T]](ll.alloc) ll.head.value = value ll.tail = ll.head ll.length++ @@ -38,7 +42,7 @@ func (ll *LinkedList[T]) init(value T) { func (ll *LinkedList[T]) popLast() T { value := ll.tail.value - mm.Free(ll.tail) + allocator.Free(ll.alloc, ll.tail) ll.tail = nil ll.head = nil ll.length-- @@ -53,7 +57,7 @@ func (ll *LinkedList[T]) PushBack(value T) { return } - newNode := mm.Alloc[linkedListNode[T]]() + newNode := allocator.Alloc[linkedListNode[T]](ll.alloc) newNode.value = value newNode.prev = ll.tail ll.tail.next = newNode @@ -69,7 +73,7 @@ func (ll *LinkedList[T]) PushFront(value T) { return } - newNode := mm.Alloc[linkedListNode[T]]() + newNode := allocator.Alloc[linkedListNode[T]](ll.alloc) newNode.value = value newNode.next = ll.head ll.head.prev = newNode @@ -90,7 +94,7 @@ func (ll *LinkedList[T]) PopBack() T { value := ll.tail.value newTail := ll.tail.prev newTail.next = nil - mm.Free(ll.tail) + allocator.Free(ll.alloc, ll.tail) ll.tail = newTail ll.length-- @@ -110,7 +114,7 @@ func (ll *LinkedList[T]) PopFront() T { value := ll.head.value newHead := ll.head.next newHead.prev = nil - mm.Free(ll.head) + allocator.Free(ll.alloc, ll.head) ll.head = newHead ll.length-- @@ -129,6 +133,21 @@ func (ll *LinkedList[T]) ForEach(f func(idx int, value T)) { } } +// Iter returns an iterator over the linked list values. +func (ll *LinkedList[T]) Iter() iter.Seq2[int, T] { + return func(yield func(int, T) bool) { + idx := 0 + currentNode := ll.head + for currentNode != nil { + if !yield(idx, currentNode.value) { + return + } + currentNode = currentNode.next + idx++ + } + } +} + func (ll *LinkedList[T]) nodeAt(idx int) *linkedListNode[T] { if idx >= ll.length { panic(fmt.Sprintf("cannot index %d in a linked list with length %d", idx, ll.length)) @@ -172,7 +191,7 @@ func (ll *LinkedList[T]) RemoveAt(idx int) T { prevNode.next = nextNode ll.length-- - mm.Free(node) + allocator.Free(ll.alloc, node) return value } @@ -267,9 +286,9 @@ func (ll *LinkedList[T]) Free() { for currentNode != nil { nextNode := currentNode.next - mm.Free(currentNode) + allocator.Free(ll.alloc, currentNode) currentNode = nextNode } - mm.Free(ll) + allocator.Free(ll.alloc, ll) } diff --git a/linkedlist/linked_list_test.go b/linkedlist/linked_list_test.go index f5df7f9..ce1c4ce 100644 --- a/linkedlist/linked_list_test.go +++ b/linkedlist/linked_list_test.go @@ -3,14 +3,18 @@ package linkedlist_test import ( "testing" - "github.com/joetifa2003/mm-go/linkedlist" "github.com/stretchr/testify/assert" + + "github.com/joetifa2003/mm-go/allocator" + "github.com/joetifa2003/mm-go/linkedlist" ) func testPushAndPop(t *testing.T) { + alloc := allocator.NewC() + assert := assert.New(t) - ll := linkedlist.New[int]() + ll := linkedlist.New[int](alloc) defer ll.Free() ll.PushBack(15) @@ -48,7 +52,9 @@ func testPushAndPop(t *testing.T) { func testForEach(t *testing.T) { assert := assert.New(t) - ll := linkedlist.New[int]() + alloc := allocator.NewC() + + ll := linkedlist.New[int](alloc) defer ll.Free() ll.PushBack(2) @@ -70,9 +76,10 @@ func testForEach(t *testing.T) { } func testIndexing(t *testing.T) { + alloc := allocator.NewC() assert := assert.New(t) - ll := linkedlist.New[int]() + ll := linkedlist.New[int](alloc) defer ll.Free() ll.PushBack(1) @@ -111,7 +118,9 @@ func testIndexing(t *testing.T) { func testRemove(t *testing.T) { assert := assert.New(t) - ll := linkedlist.New[int]() + alloc := allocator.NewC() + + ll := linkedlist.New[int](alloc) defer ll.Free() ll.PushBack(1) diff --git a/malloc/malloc.go b/malloc/malloc.go deleted file mode 100644 index 1bef16b..0000000 --- a/malloc/malloc.go +++ /dev/null @@ -1,51 +0,0 @@ -package malloc - -import ( - "fmt" - "runtime" - "unsafe" - - "github.com/ebitengine/purego" -) - -var calloc func(n int, size int) unsafe.Pointer -var realloc func(ptr unsafe.Pointer, size int) unsafe.Pointer -var free func(ptr unsafe.Pointer) - -func getSystemLibrary() string { - switch runtime.GOOS { - case "darwin": - return "/usr/lib/libSystem.B.dylib" - case "linux": - return "libc.so.6" - case "windows": - return "ucrtbase.dll" - default: - panic(fmt.Errorf("GOOS=%s is not supported", runtime.GOOS)) - } -} - -func init() { - libc, err := openLibrary(getSystemLibrary()) - if err != nil { - panic(err) - } - purego.RegisterLibFunc(&calloc, libc, "calloc") - purego.RegisterLibFunc(&realloc, libc, "realloc") - purego.RegisterLibFunc(&free, libc, "free") -} - -// CMalloc raw binding to c calloc(1, size) -func Malloc(size int) unsafe.Pointer { - return calloc(1, size) -} - -// CMalloc raw binding to c free -func Free(ptr unsafe.Pointer) { - free(ptr) -} - -// CMalloc raw binding to c realloc -func Realloc(ptr unsafe.Pointer, size int) unsafe.Pointer { - return realloc(ptr, size) -} diff --git a/malloc/malloc_unix.go b/malloc/malloc_unix.go deleted file mode 100644 index b4e3b63..0000000 --- a/malloc/malloc_unix.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build darwin || linux - -package malloc - -import "github.com/ebitengine/purego" - -func openLibrary(name string) (uintptr, error) { - return purego.Dlopen(name, purego.RTLD_NOW|purego.RTLD_GLOBAL) -} diff --git a/malloc/malloc_windows.go b/malloc/malloc_windows.go deleted file mode 100644 index 62bfb23..0000000 --- a/malloc/malloc_windows.go +++ /dev/null @@ -1,10 +0,0 @@ -//go:build windows - -package malloc - -import "golang.org/x/sys/windows" - -func openLibrary(name string) (uintptr, error) { - handle, err := windows.LoadLibrary(name) - return uintptr(handle), err -} diff --git a/minheap/example_test.go b/minheap/example_test.go new file mode 100644 index 0000000..de43fb8 --- /dev/null +++ b/minheap/example_test.go @@ -0,0 +1,56 @@ +package minheap_test + +import ( + "fmt" + + "github.com/joetifa2003/mm-go/allocator" + "github.com/joetifa2003/mm-go/minheap" +) + +func int_less(a, b int) bool { return a < b } + +func Example() { + alloc := allocator.NewC() + defer alloc.Destroy() + + h := minheap.New[int](alloc, int_less) + + // Push some values onto the heap + h.Push(2) + h.Push(1) + h.Push(4) + h.Push(3) + h.Push(5) + + // Pop the minimum value from the heap + fmt.Println(h.Pop()) + fmt.Println(h.Pop()) + + // Output: + // 1 + // 2 +} + +func int_greater(a, b int) bool { return a > b } + +func Example_MaxHeap() { + alloc := allocator.NewC() + defer alloc.Destroy() + + h := minheap.New[int](alloc, int_greater) + + // Push some values onto the heap + h.Push(2) + h.Push(1) + h.Push(4) + h.Push(3) + h.Push(5) + + // Pop the max value from the heap + fmt.Println(h.Pop()) + fmt.Println(h.Pop()) + + // Output: + // 5 + // 4 +} diff --git a/minheap/minheap.go b/minheap/minheap.go new file mode 100644 index 0000000..179084e --- /dev/null +++ b/minheap/minheap.go @@ -0,0 +1,127 @@ +package minheap + +import ( + "iter" + + "github.com/joetifa2003/mm-go/allocator" + "github.com/joetifa2003/mm-go/vector" +) + +type MinHeap[T any] struct { + alloc allocator.Allocator + data *vector.Vector[T] + less func(a, b T) bool // check if a < b +} + +// New creates a new MinHeap. +func New[T any](alloc allocator.Allocator, less func(a, b T) bool) *MinHeap[T] { + minHeap := allocator.Alloc[MinHeap[T]](alloc) + minHeap.alloc = alloc + minHeap.data = vector.New[T](alloc) // Start with an initial capacity of 16 + minHeap.less = less + return minHeap +} + +// Push adds a value to the heap. +func (h *MinHeap[T]) Push(value T) { + h.data.Push(value) + h.heapifyUp(h.data.Len() - 1) +} + +// Pop removes and returns the minimum value from the heap. +func (h *MinHeap[T]) Pop() T { + if h.data.Len() == 0 { + panic("cannot pop from empty heap") + } + + minValue := h.data.RemoveAt(0) + h.heapifyDown(0) + + return minValue +} + +// Peek returns the minimum value from the heap without removing it. +func (h *MinHeap[T]) Peek() T { + if h.data.Len() == 0 { + panic("cannot peek into empty heap") + } + return h.data.UnsafeAt(0) +} + +// Len returns the number of elements in the heap. +func (h *MinHeap[T]) Len() int { + return h.data.Len() +} + +// Free frees the heap. +func (h *MinHeap[T]) Free() { + h.data.Free() + allocator.Free(h.alloc, h) +} + +// Remove the first element that makes f return true +func (h *MinHeap[T]) Remove(f func(T) bool) { + for i := 0; i < h.data.Len(); i++ { + if f(h.data.UnsafeAt(i)) { + h.removeAt(i) + return + } + } +} + +func (h *MinHeap[T]) heapifyUp(index int) { + for index > 0 { + parentIndex := (index - 1) / 2 + if h.less(h.data.UnsafeAt(parentIndex), h.data.At(index)) { + break + } + h.swap(parentIndex, index) + index = parentIndex + } +} + +func (h *MinHeap[T]) heapifyDown(index int) { + for { + leftChildIndex := 2*index + 1 + rightChildIndex := 2*index + 2 + smallestIndex := index + + if leftChildIndex < h.data.Len() && h.less(h.data.UnsafeAt(leftChildIndex), h.data.At(smallestIndex)) { + smallestIndex = leftChildIndex + } + + if rightChildIndex < h.data.Len() && h.less(h.data.UnsafeAt(rightChildIndex), h.data.At(smallestIndex)) { + smallestIndex = rightChildIndex + } + + if smallestIndex == index { + break + } + + h.swap(index, smallestIndex) + index = smallestIndex + } +} + +func (h *MinHeap[T]) swap(i, j int) { + temp := h.data.UnsafeAt(i) + h.data.Set(i, h.data.UnsafeAt(j)) + h.data.Set(j, temp) +} + +// removeAt removes the element at the specified index from the heap. +func (h *MinHeap[T]) removeAt(index int) { + if index == h.data.Len()-1 { + h.data.Pop() + } else { + h.swap(index, h.data.Len()-1) + h.data.Pop() + h.heapifyDown(index) + h.heapifyUp(index) + } +} + +// Iter returns an iterator over the elements of the heap. +func (h *MinHeap[T]) Iter() iter.Seq2[int, T] { + return h.data.Iter() +} diff --git a/minheap/minheap_test.go b/minheap/minheap_test.go new file mode 100644 index 0000000..8551e65 --- /dev/null +++ b/minheap/minheap_test.go @@ -0,0 +1,17 @@ +package minheap + +import ( + "testing" + + "github.com/joetifa2003/mm-go/allocator" +) + +func TestMinHeap(t *testing.T) { + alloc := allocator.NewC() + heap := New[int](alloc, func(a, b int) bool { return a < b }) + + heap.Push(3) + heap.Push(4) + heap.Push(1) + heap.Push(0) +} diff --git a/mm.go b/mm.go index f733ed9..426ca5a 100644 --- a/mm.go +++ b/mm.go @@ -1,50 +1,15 @@ package mm -import ( - "unsafe" +import "unsafe" - "github.com/joetifa2003/mm-go/malloc" -) - -func getSize[T any]() int { +// SizeOf returns the size of T in bytes +func SizeOf[T any]() int { var zeroV T return int(unsafe.Sizeof(zeroV)) } -// Alloc allocates T and returns a pointer to it. -func Alloc[T any]() *T { - ptr := malloc.Malloc(getSize[T]()) - return (*T)(unsafe.Pointer(ptr)) -} - -// FreeMany frees memory allocated by Alloc takes a ptr -// CAUTION: be careful not to double free, and prefer using defer to deallocate -func Free[T any](ptr *T) { - malloc.Free(unsafe.Pointer(ptr)) -} - -// AllocMany allocates n of T and returns a slice representing the heap. -// CAUTION: don't append to the slice, the purpose of it is to replace pointer -// arithmetic with slice indexing -func AllocMany[T any](n int) []T { - ptr := malloc.Malloc(getSize[T]() * n) - return unsafe.Slice( - (*T)(ptr), - n, - ) -} - -// FreeMany frees memory allocated by AllocMany takes in the slice (aka the heap) -// CAUTION: be careful not to double free, and prefer using defer to deallocate -func FreeMany[T any](slice []T) { - malloc.Free(unsafe.Pointer(&slice[0])) -} - -// Reallocate reallocates memory allocated with AllocMany and doesn't change underling data -func Reallocate[T any](slice []T, newN int) []T { - ptr := malloc.Realloc(unsafe.Pointer(&slice[0]), getSize[T]()*newN) - return unsafe.Slice( - (*T)(ptr), - newN, - ) +// Zero returns a zero value of T +func Zero[T any]() T { + var zeroV T + return zeroV } diff --git a/mm_test.go b/mm_test.go index 4b609b3..5e2c30f 100644 --- a/mm_test.go +++ b/mm_test.go @@ -6,214 +6,173 @@ import ( "testing" "github.com/joetifa2003/mm-go" + "github.com/joetifa2003/mm-go/allocator" + "github.com/joetifa2003/mm-go/batchallocator" "github.com/joetifa2003/mm-go/typedarena" - "github.com/stretchr/testify/assert" ) -type Node struct { - Value int - Prev *Node - Next *Node +type Node[T any] struct { + value T + next *Node[T] + prev *Node[T] } -func heapManaged(nodes int) { - allocated := make([]*Node, nodes) - - for j := 0; j < nodes; j++ { - var prev *Node - var next *Node - if j != 0 { - prev = allocated[j-1] - } - if j != nodes-1 { - next = allocated[j+1] - } +type LinkedList[T any] struct { + head *Node[T] + tail *Node[T] +} - allocated[j] = &Node{ - Value: j, - Prev: prev, - Next: next, - } +func linkedListPushManaged[T any](list *LinkedList[T], value T) { + node := &Node[T]{value: value} + if list.head == nil { + list.head = node + list.tail = node + } else { + list.tail.next = node + node.prev = list.tail + list.tail = node } - - runtime.GC() } -func BenchmarkHeapManaged(b *testing.B) { - benchMarkSuit(b, heapManaged) -} +func linkedListPushAlloc[T any](alloc allocator.Allocator, list *LinkedList[T], value T) { + node := allocator.Alloc[Node[T]](alloc) + node.value = value -func manual(nodes int) { - allocatedNodes := mm.AllocMany[Node](nodes) + if list.head == nil { + list.head = node + list.tail = node + } else { + list.tail.next = node + node.prev = list.tail + list.tail = node + } +} - for j := 0; j < nodes; j++ { - var prev *Node - var next *Node - if j != 0 { - prev = &allocatedNodes[j-1] - } - if j != nodes-1 { - next = &allocatedNodes[j+1] - } +func linkedListPushArena[T any](arena *typedarena.TypedArena[Node[T]], list *LinkedList[T], value T) { + node := arena.Alloc() + node.value = value - allocatedNodes[j] = Node{ - Value: j, - Prev: prev, - Next: next, - } + if list.head == nil { + list.head = node + list.tail = node + } else { + list.tail.next = node + node.prev = list.tail + list.tail = node } - - mm.FreeMany(allocatedNodes) - runtime.GC() } -func BenchmarkManual(b *testing.B) { - benchMarkSuit(b, manual) +func linkedListFree[T any](alloc allocator.Allocator, list *LinkedList[T]) { + currentNode := list.head + for currentNode != nil { + nextNode := currentNode.next + allocator.Free(alloc, currentNode) + currentNode = nextNode + } } -func arenaManual(nodes int) { - arena := typedarena.New[Node](nodes) - allocatedNodes := arena.AllocMany(nodes) - - for j := 0; j < nodes; j++ { - var prev *Node - var next *Node - if j != 0 { - prev = &allocatedNodes[j-1] - } - if j != nodes-1 { - next = &allocatedNodes[j+1] - } +const LINKED_LIST_SIZE = 10000 - allocatedNodes[j] = Node{ - Value: j, - Prev: prev, - Next: next, - } +func BenchmarkLinkedListManaged(b *testing.B) { + for range b.N { + benchLinkedListManaged(b, LINKED_LIST_SIZE) + runtime.GC() } - - arena.Free() - runtime.GC() } -func BenchmarkArenaManual(b *testing.B) { - benchMarkSuit(b, arenaManual) +func BenchmarkLinkedListCAlloc(b *testing.B) { + for range b.N { + benchLinkedListCAlloc(b, LINKED_LIST_SIZE) + } } -func benchMarkSuit(b *testing.B, f func(int)) { - nodeCounts := []int{10000, 100000, 10000000, 100000000} - for _, nc := range nodeCounts { - b.Run(fmt.Sprintf("node count %d", nc), func(b *testing.B) { - for i := 0; i < b.N; i++ { - f(nc) +func BenchmarkLinkedListBatchAllocator(b *testing.B) { + for _, bucketSize := range []int{100, 200, 500, LINKED_LIST_SIZE} { + b.Run(fmt.Sprintf("bucket size %d", bucketSize), func(b *testing.B) { + for range b.N { + benchLinkedListBatchAllocator(b, LINKED_LIST_SIZE, bucketSize) } }) } } -type TreeNode struct { - value int - left, right *TreeNode -} - -func createTreeManaged(depth int) *TreeNode { - if depth != 0 { - return &TreeNode{ - value: depth, - left: createTreeManaged(depth - 1), - right: createTreeManaged(depth - 1), - } +func BenchmarkLinkedListTypedArena(b *testing.B) { + for _, chunkSize := range []int{100, 200, 500, LINKED_LIST_SIZE} { + b.Run(fmt.Sprintf("chunk size %d", chunkSize), func(b *testing.B) { + for range b.N { + benchLinkedListTypedArena(b, LINKED_LIST_SIZE, chunkSize) + } + }) } - - return nil } -func createTreeManual(depth int, arena *typedarena.TypedArena[TreeNode]) *TreeNode { - if depth != 0 { - node := arena.Alloc() - node.left = createTreeManual(depth-1, arena) - node.right = createTreeManual(depth-1, arena) - return node - } +func benchLinkedListTypedArena(b *testing.B, size int, chunkSize int) { + alloc := allocator.NewC() + defer alloc.Destroy() - return nil -} + arena := typedarena.New[Node[int]](alloc, chunkSize) + defer arena.Free() -func sumBinaryTree(tree *TreeNode) int { - if tree.left == nil && tree.right == nil { - return tree.value + list := allocator.Alloc[LinkedList[int]](alloc) + defer allocator.Free(alloc, list) + + for i := range size { + linkedListPushArena(arena, list, i) } - return sumBinaryTree(tree.left) + sumBinaryTree(tree.right) + assertLinkedList(b, list) } -const TREE_DEPTH = 26 - -func BenchmarkBinaryTreeManaged(b *testing.B) { - for n := 0; n < b.N; n++ { - tree := createTreeManaged(TREE_DEPTH) - runtime.GC() - sumBinaryTree(tree) +func benchLinkedListManaged(b *testing.B, size int) { + list := &LinkedList[int]{} + for i := range size { + linkedListPushManaged(list, i) } + assertLinkedList(b, list) } -func BenchmarkBinaryTreeArena(b *testing.B) { - for _, chunkSize := range []int{50, 100, 150, 250, 500} { - b.Run(fmt.Sprintf("chunk size %d", chunkSize), func(b *testing.B) { - for n := 0; n < b.N; n++ { - arena := typedarena.New[TreeNode](chunkSize) - tree := createTreeManual(TREE_DEPTH, arena) - runtime.GC() - sumBinaryTree(tree) - arena.Free() - } - }) - } -} +func benchLinkedListCAlloc(b *testing.B, size int) { + alloc := allocator.NewC() + defer alloc.Destroy() -func TestAllocMany(t *testing.T) { - assert := assert.New(t) + list := allocator.Alloc[LinkedList[int]](alloc) + defer linkedListFree(alloc, list) - 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 data in the heap - *ptr = 45 // changes the value from 15 to 45 + for i := range size { + linkedListPushAlloc(alloc, list, i) + } - assert.Equal(45, allocated[0]) + assertLinkedList(b, list) } -func TestAlloc(t *testing.T) { - assert := assert.New(t) +func benchLinkedListBatchAllocator(b *testing.B, size int, bucketSize int) { + alloc := batchallocator.New(allocator.NewC(), + batchallocator.WithBucketSize(mm.SizeOf[Node[int]]()*bucketSize), + ) + defer alloc.Destroy() - 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) - - *ptr = 15 - assert.Equal(15, *ptr) - - ptr2 := mm.Alloc[[1e3]int]() // creates large array to make malloc mmap new chunk - defer mm.Free(ptr2) + list := allocator.Alloc[LinkedList[int]](alloc) + for i := range size { + linkedListPushAlloc(alloc, list, i) + } + assertLinkedList(b, list) } -func TestReallocate(t *testing.T) { - assert := assert.New(t) - - 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) -} +func assertLinkedList(t *testing.B, list *LinkedList[int]) { + if list.head == nil { + t.Fatal("list head is nil") + } + if list.tail == nil { + t.Fatal("list tail is nil") + } -func TestUnmapChunk(t *testing.T) { - data1 := mm.AllocMany[int](1e6) - data2 := mm.AllocMany[int](1e6) - data3 := mm.AllocMany[int](1e6) - mm.FreeMany(data2) - mm.FreeMany(data1) - mm.FreeMany(data3) + currentNode := list.head + i := 0 + for currentNode != nil { + if currentNode.value != i { + t.Fatalf("list value at index %d is %d, expected %d", i, currentNode.value, i) + } + i++ + currentNode = currentNode.next + } } diff --git a/mmstring/example_test.go b/mmstring/example_test.go new file mode 100644 index 0000000..78bcbf3 --- /dev/null +++ b/mmstring/example_test.go @@ -0,0 +1,48 @@ +package mmstring_test + +import ( + "fmt" + + "github.com/joetifa2003/mm-go/allocator" + "github.com/joetifa2003/mm-go/batchallocator" + "github.com/joetifa2003/mm-go/mmstring" + "github.com/joetifa2003/mm-go/vector" +) + +func Example() { + alloc := allocator.NewC() + defer alloc.Destroy() + + s := mmstring.New(alloc) + defer s.Free() + + s.AppendGoString("Hello ") + s.AppendGoString("World") + + s2 := mmstring.From(alloc, "Foo Bar") + defer s2.Free() + + fmt.Println(s.GetGoString()) + fmt.Println(s2.GetGoString()) + + // Output: + // Hello World + // Foo Bar +} + +func Example_datastructures() { + alloc := batchallocator.New(allocator.NewC()) + defer alloc.Destroy() // all the memory allocated bellow will be freed, no need to free it manually. + + m := vector.New[*mmstring.MMString](alloc) + m.Push(mmstring.From(alloc, "hello")) + m.Push(mmstring.From(alloc, "world")) + + for k, v := range m.Iter() { + fmt.Println(k, v.GetGoString()) + } + + // Output: + // 0 hello + // 1 world +} diff --git a/mmstring/string.go b/mmstring/string.go index 727120b..800c700 100644 --- a/mmstring/string.go +++ b/mmstring/string.go @@ -1,9 +1,7 @@ package mmstring import ( - "hash/fnv" - - "github.com/joetifa2003/mm-go" + "github.com/joetifa2003/mm-go/allocator" "github.com/joetifa2003/mm-go/vector" ) @@ -11,19 +9,21 @@ import ( // and contains all the methods of a vector plus additional helper functions type MMString struct { *vector.Vector[rune] + alloc allocator.Allocator } // New create a new manually managed string -func New() *MMString { - mmString := mm.Alloc[MMString]() - mmString.Vector = vector.New[rune]() +func New(alloc allocator.Allocator) *MMString { + mmString := allocator.Alloc[MMString](alloc) + mmString.Vector = vector.New[rune](alloc) + mmString.alloc = alloc return mmString } // From creates a new manually managed string, // And initialize it with a go string -func From(input string) *MMString { - mmString := New() +func From(alloc allocator.Allocator, input string) *MMString { + mmString := New(alloc) for _, r := range input { mmString.Push(r) @@ -46,16 +46,8 @@ func (s *MMString) AppendGoString(input string) { } } -// Hash implements Hashable interface -func (s *MMString) Hash() uint32 { - runes := s.Slice() - h := fnv.New32a() - h.Write([]byte(string(runes))) - return 0 -} - // Free frees MMString func (s *MMString) Free() { s.Vector.Free() - mm.Free(s) + allocator.Free(s.alloc, s) } diff --git a/mmstring/string_test.go b/mmstring/string_test.go index c1c574b..78c9e07 100644 --- a/mmstring/string_test.go +++ b/mmstring/string_test.go @@ -3,13 +3,16 @@ package mmstring_test import ( "testing" - "github.com/joetifa2003/mm-go/mmstring" "github.com/stretchr/testify/assert" + + "github.com/joetifa2003/mm-go/allocator" + "github.com/joetifa2003/mm-go/mmstring" ) func TestString(t *testing.T) { + alloc := allocator.NewC() assert := assert.New(t) - mmString := mmstring.From("hi") + mmString := mmstring.From(alloc, "hi") defer mmString.Free() assert.Equal('h', mmString.At(0)) diff --git a/typedarena/example_test.go b/typedarena/example_test.go new file mode 100644 index 0000000..d93ac22 --- /dev/null +++ b/typedarena/example_test.go @@ -0,0 +1,51 @@ +package typedarena_test + +import ( + "fmt" + + "github.com/joetifa2003/mm-go/allocator" + "github.com/joetifa2003/mm-go/typedarena" +) + +type Entity struct { + VelocityX float32 + VelocityY float32 + PositionX float32 + PositionY float32 +} + +func Example() { + alloc := allocator.NewC() + defer alloc.Destroy() + + arena := typedarena.New[Entity]( + alloc, + 10, + ) + defer arena.Free() // frees all memory + + for i := 0; i < 10; i++ { + e := arena.Alloc() // *Entity + e.VelocityX = float32(i) + e.VelocityY = float32(i) + e.PositionX = float32(i) + e.PositionY = float32(i) + fmt.Println(e.VelocityX, e.VelocityY, e.PositionX, e.PositionY) + } + + entities := arena.AllocMany(10) // allocate slice of 10 entities (cannot exceed 10 here because chunk size is 10 above, this limitation doesn't exist in batchallocator) + + _ = entities + + // Output: + // 0 0 0 0 + // 1 1 1 1 + // 2 2 2 2 + // 3 3 3 3 + // 4 4 4 4 + // 5 5 5 5 + // 6 6 6 6 + // 7 7 7 7 + // 8 8 8 8 + // 9 9 9 9 +} diff --git a/typedarena/typed_arena.go b/typedarena/typed_arena.go index 9dbe532..b6039ba 100644 --- a/typedarena/typed_arena.go +++ b/typedarena/typed_arena.go @@ -1,18 +1,21 @@ +// typedarena is a growable typed arena that allocates memory in fixed chunks , it's faster that batchallocator but more limited, you can use batchallocator if you want to allocate multiple different types, and you want to use an arena like behavior spanning multiple datastructures (like vector, linkedlist, hashmap etc..), typedarena is much faster when you are only allocating one type. package typedarena import ( - "github.com/joetifa2003/mm-go" + "github.com/joetifa2003/mm-go/allocator" "github.com/joetifa2003/mm-go/vector" ) type typedChunk[T any] struct { - data []T - len int + data []T + len int + alloc allocator.Allocator } -func newChunk[T any](size int) *typedChunk[T] { - chunk := mm.Alloc[typedChunk[T]]() - chunk.data = mm.AllocMany[T](size) +func newChunk[T any](alloc allocator.Allocator, size int) *typedChunk[T] { + chunk := allocator.Alloc[typedChunk[T]](alloc) + chunk.data = allocator.AllocMany[T](alloc, size) + chunk.alloc = alloc return chunk } @@ -29,14 +32,15 @@ func (c *typedChunk[T]) AllocMany(n int) []T { } func (c *typedChunk[T]) Free() { - mm.FreeMany(c.data) - mm.Free(c) + allocator.FreeMany(c.alloc, c.data) + allocator.Free(c.alloc, c) } // TypedArena is a growable typed arena type TypedArena[T any] struct { chunks *vector.Vector[*typedChunk[T]] chunkSize int + alloc allocator.Allocator } // New creates a typed arena with the specified chunk size. @@ -44,12 +48,13 @@ type TypedArena[T any] struct { // 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 -func New[T any](chunkSize int) *TypedArena[T] { - tArena := mm.Alloc[TypedArena[T]]() +func New[T any](alloc allocator.Allocator, chunkSize int) *TypedArena[T] { + tArena := allocator.Alloc[TypedArena[T]](alloc) tArena.chunkSize = chunkSize - tArena.chunks = vector.New[*typedChunk[T]]() + tArena.chunks = vector.New[*typedChunk[T]](alloc) + tArena.alloc = alloc - firstChunk := newChunk[T](chunkSize) + firstChunk := newChunk[T](alloc, chunkSize) tArena.chunks.Push(firstChunk) return tArena @@ -59,7 +64,7 @@ func New[T any](chunkSize int) *TypedArena[T] { func (ta *TypedArena[T]) Alloc() *T { lastChunk := ta.chunks.Last() if lastChunk.len == ta.chunkSize { - nc := newChunk[T](ta.chunkSize) + nc := newChunk[T](ta.alloc, ta.chunkSize) ta.chunks.Push(nc) return nc.Alloc() } @@ -77,7 +82,7 @@ func (ta *TypedArena[T]) AllocMany(n int) []T { lastChunk := ta.chunks.Last() if lastChunk.len+n > ta.chunkSize { - nc := newChunk[T](ta.chunkSize) + nc := newChunk[T](ta.alloc, ta.chunkSize) ta.chunks.Push(nc) return nc.AllocMany(n) } @@ -91,5 +96,5 @@ func (ta *TypedArena[T]) Free() { c.Free() } ta.chunks.Free() - mm.Free(ta) + allocator.Free(ta.alloc, ta) } diff --git a/typedarena/typed_test.go b/typedarena/typed_test.go index 6433bc4..f016094 100644 --- a/typedarena/typed_test.go +++ b/typedarena/typed_test.go @@ -3,14 +3,17 @@ package typedarena_test import ( "testing" - "github.com/joetifa2003/mm-go/typedarena" "github.com/stretchr/testify/assert" + + "github.com/joetifa2003/mm-go/allocator" + "github.com/joetifa2003/mm-go/typedarena" ) func TestTypedArena(t *testing.T) { + alloc := allocator.NewC() assert := assert.New(t) - arena := typedarena.New[int](4) + arena := typedarena.New[int](alloc, 4) defer arena.Free() int1 := arena.Alloc() // allocates 1 int from arena diff --git a/vector/example_test.go b/vector/example_test.go new file mode 100644 index 0000000..a942ee0 --- /dev/null +++ b/vector/example_test.go @@ -0,0 +1,34 @@ +package vector_test + +import ( + "fmt" + + "github.com/joetifa2003/mm-go/allocator" + "github.com/joetifa2003/mm-go/vector" +) + +func Example() { + alloc := allocator.NewC() + v := vector.New[int](alloc) + v.Push(1) + v.Push(2) + v.Push(3) + + fmt.Println("Length:", v.Len()) + for i := 0; i < v.Len(); i++ { + fmt.Println(v.At(i)) + } + + for _, k := range v.Iter() { + fmt.Println(k) + } + + // Output: + // Length: 3 + // 1 + // 2 + // 3 + // 1 + // 2 + // 3 +} diff --git a/vector/vector.go b/vector/vector.go index c70a70a..18e1dd1 100644 --- a/vector/vector.go +++ b/vector/vector.go @@ -2,20 +2,23 @@ package vector import ( "fmt" + "iter" - "github.com/joetifa2003/mm-go" + "github.com/joetifa2003/mm-go/allocator" ) // Vector a contiguous growable array type type Vector[T any] struct { - data []T - len int + data []T + len int + alloc allocator.Allocator } -func createVector[T any](len int, cap int) *Vector[T] { - vector := mm.Alloc[Vector[T]]() +func createVector[T any](alloc allocator.Allocator, len int, cap int) *Vector[T] { + vector := allocator.Alloc[Vector[T]](alloc) vector.len = len - vector.data = mm.AllocMany[T](cap) + vector.data = allocator.AllocMany[T](alloc, cap) + vector.alloc = alloc return vector } @@ -24,21 +27,21 @@ func createVector[T any](len int, cap int) *Vector[T] { // 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] { +func New[T any](aloc allocator.Allocator, args ...int) *Vector[T] { switch len(args) { case 0: - return createVector[T](0, 1) + return createVector[T](aloc, 0, 1) case 1: - return createVector[T](args[0], args[0]) + return createVector[T](aloc, args[0], args[0]) default: - return createVector[T](args[0], args[1]) + return createVector[T](aloc, args[0], args[1]) } } // 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] { - vector := createVector[T](len(values), len(values)) +func Init[T any](alloc allocator.Allocator, values ...T) *Vector[T] { + vector := createVector[T](alloc, len(values), len(values)) copy(vector.data, values) return vector } @@ -46,7 +49,7 @@ func Init[T any](values ...T) *Vector[T] { // Push pushes value T to the vector, grows if needed. func (v *Vector[T]) Push(value T) { if v.len == v.Cap() { - v.data = mm.Reallocate(v.data, v.Cap()*2) + v.data = allocator.Realloc(v.alloc, v.data, v.Cap()*2) } v.data[v.len] = value @@ -90,6 +93,11 @@ func (v *Vector[T]) At(idx int) T { return v.data[idx] } +// UnsafeAT gets element T at specified index without bounds checking +func (v *Vector[T]) UnsafeAt(idx int) T { + return v.data[idx] +} + // AtPtr gets element a pointer of T at specified index func (v *Vector[T]) AtPtr(idx int) *T { if idx >= v.len { @@ -110,6 +118,29 @@ func (v *Vector[T]) Set(idx int, value T) { // Free deallocats the vector func (v *Vector[T]) Free() { - mm.FreeMany(v.data) - mm.Free(v) + allocator.FreeMany[T](v.alloc, v.data) + allocator.Free(v.alloc, v) +} + +func (v *Vector[T]) RemoveAt(idx int) T { + if idx >= v.len { + panic(fmt.Sprintf("cannot remove %d in a vector with length %d", idx, v.len)) + } + + tmp := v.data[idx] + v.data[idx] = v.data[v.len-1] + v.len-- + + return tmp +} + +// Iter iterates over the vector +func (v *Vector[T]) Iter() iter.Seq2[int, T] { + return func(yield func(int, T) bool) { + for i := 0; i < v.len; i++ { + if !yield(i, v.data[i]) { + return + } + } + } } diff --git a/vector/vector_test.go b/vector/vector_test.go index 27e014b..aa73529 100644 --- a/vector/vector_test.go +++ b/vector/vector_test.go @@ -3,14 +3,18 @@ package vector_test import ( "testing" - "github.com/joetifa2003/mm-go/vector" "github.com/stretchr/testify/assert" + + "github.com/joetifa2003/mm-go/allocator" + "github.com/joetifa2003/mm-go/vector" ) func TestVector(t *testing.T) { + alloc := allocator.NewC() + assert := assert.New(t) - v := vector.New[int]() + v := vector.New[int](alloc) defer v.Free() v.Push(1) @@ -36,9 +40,10 @@ func TestVector(t *testing.T) { func TestVectorInit(t *testing.T) { t.Run("Init with no args", func(t *testing.T) { + alloc := allocator.NewC() assert := assert.New(t) - v := vector.New[int]() + v := vector.New[int](alloc) defer v.Free() assert.Equal(0, v.Len()) @@ -46,9 +51,10 @@ func TestVectorInit(t *testing.T) { }) t.Run("Init with one arg", func(t *testing.T) { + alloc := allocator.NewC() assert := assert.New(t) - v := vector.New[int](5) + v := vector.New[int](alloc, 5) defer v.Free() assert.Equal(5, v.Len()) @@ -56,9 +62,10 @@ func TestVectorInit(t *testing.T) { }) t.Run("Init with two args", func(t *testing.T) { + alloc := allocator.NewC() assert := assert.New(t) - v := vector.New[int](5, 6) + v := vector.New[int](alloc, 5, 6) defer v.Free() assert.Equal(5, v.Len()) @@ -66,9 +73,10 @@ func TestVectorInit(t *testing.T) { }) t.Run("Init vector with slice", func(t *testing.T) { + alloc := allocator.NewC() assert := assert.New(t) - v := vector.Init(1, 2, 3) + v := vector.Init(alloc, 1, 2, 3) defer v.Free() assert.Equal(3, v.Len())