mm-go
Generic manual memory management for golang
About mm-go
This large README is shown as a plain-text preview. Read the full README at the source
[](https://goreportcard.com/report/github.com/joetifa2003/mm-go)
[](https://pkg.go.dev/github.com/joetifa2003/mm-go)
# mm-go Generic manual memory management for golang
Golang manages memory via GC and it's good for almost every use case but sometimes it can be a bottleneck.
and this is where mm-go comes in to play.
## 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**.
- 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 batchallocator/Arena/AllocMany).
- Check the docs, test files and read the README.
## Star History
[](https://star-history.com/#joetifa2003/mm-go&Date)
## Installing
```
go get -u github.com/joetifa2003/mm-go
```
## 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
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
```
<!-- gomarkdoc:embed:start -->
<!-- Code generated by gomarkdoc. DO NOT EDIT -->
# mm
```go
import "github.com/joetifa2003/mm-go"
```
## Index
- [func SizeOf\[T any\]\(\) int](<#SizeOf>)
- [func Zero\[T any\]\(\) T](<#Zero>)
<a name="SizeOf"></a>
## func [SizeOf](<https://github.com/joetifa2003/mm-go/blob/master/mm.go#L6>)
```go
func SizeOf[T any]() int
```
SizeOf returns the size of T in bytes
<details><summary>Example</summary>
<p>
```go
fmt.Println(mm.SizeOf[int32]())
fmt.Println(mm.SizeOf[int64]())
// Output:
// 4
// 8
```
#### Output
```
4
8
```
</p>
</details>
<a name="Zero"></a>
## func [Zero](<https://github.com/joetifa2003/mm-go/blob/master/mm.go#L12>)
```go
func Zero[T any]() T
```
Zero returns a zero value of T
# allocator
```go
import "github.com/joetifa2003/mm-go/allocator"
```
<details><summary>Example</summary>
<p>
```go
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)
}
```
#### Output
```
15
```
</p>
</details>
<details><summary>Example (Datastructures)</summary>
<p>
```go
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
```
{100 200}
15
70
hello
world
```
</p>
</details>
## 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>)
<a name="Alloc"></a>
## func [Alloc](<https://github.com/joetifa2003/mm-go/blob/master/allocator/allocator.go#L60>)
```go
func Alloc[T any](a Allocator) *T
```
Alloc allocates T and returns a pointer to it.
<details><summary>Example</summary>
<p>
```go
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
```
#### Output
```
15
15
```
</p>
</details>
<a name="AllocMany"></a>
## func [AllocMany](<https://github.com/joetifa2003/mm-go/blob/master/allocator/allocator.go#L74>)
```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
<details><summary>Example</summary>
<p>
```go
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 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
```
</p>
</details>
<a name="Free"></a>
## func [Free](<https://github.com/joetifa2003/mm-go/blob/master/allocator/allocator.go#L67>)
```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
<a name="FreeMany"></a>
## func [FreeMany](<https://github.com/joetifa2003/mm-go/blob/master/allocator/allocator.go#L84>)
```go
func FreeMany[T any](a Allocator, slice []T)
```
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
<a name="Realloc"></a>
## func [Realloc](<https://github.com/joetifa2003/mm-go/blob/master/allocator/allocator.go#L89>)
```go
func Realloc[T any](a Allocator, slice []T, newN int) []T
```
Realloc reallocates memory allocated with AllocMany and doesn't change underling data
<details><summary>Example</summary>
<p>
```go
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)
}
```
#### Output
```
15
70
100
```
</p>
</details>
<a name="Allocator"></a>
## type [Allocator](<https://github.com/joetifa2003/mm-go/blob/master/allocator/allocator.go#L7-L13>)
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
type Allocator struct {
// contains filtered or unexported fields
}
```
<a name="NewAllocator"></a>
### func [NewAllocator](<https://github.com/joetifa2003/mm-go/blob/master/allocator/allocator.go#L16-L22>)
```go
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 creates a new Allocator
<details><summary>Example</summary>
<p>
```go
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) {
}
```
</p>
</details>
<a name="NewC"></a>
### func [NewC](<https://github.com/joetifa2003/mm-go/blob/master/allocator/callocator.go#L9>)
```go
func NewC() Allocator
```
NewC returns an allocator that uses C calloc, realloc and free.
<a name="Allocator.Alloc"></a>
### func \(Allocator\) [Alloc](<https://github.com/joetifa2003/mm-go/blob/master/allocator/allocator.go#L33>)
```go
func (a Allocator) Alloc(size int) unsafe.Pointer
```
Alloc allocates size bytes and returns an unsafe pointer to it.
<a name="Allocator.Destroy"></a>
### func \(Allocator\) [Destroy](<https://github.com/joetifa2003/mm-go/blob/master/allocator/allocator.go#L50>)
```go
func (a Allocator) Destroy()
```
Destroy destroys the allocator. After calling this, the allocator is no longer usable. This is useful for cleanup, freeing allocator internal resources, etc.
<a name="Allocator.Free"></a>
### func \(Allocator\) [Free](<https://github.com/joetifa2003/mm-go/blob/master/allocator/allocator.go#L38>)
```go
func (a Allocator) Free(ptr unsafe.Pointer)
```
Free frees the memory pointed by ptr
<a name="Allocator.Realloc"></a>
### func \(Allocator\) [Realloc](<https://github.com/joetifa2003/mm-go/blob/master/allocator/allocator.go#L43>)
```go
func (a Allocator) Realloc(ptr unsafe.Pointer, size int) unsafe.Pointer
```
Realloc reallocates the memory pointed by ptr with a new size and returns a new pointer to it.
# batchallocator
```go
import "github.com/joetifa2003/mm-go/batchallocator"
```
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.
<details><summary>Example</summary>
<p>
```go
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.
}
```
</p>
</details>
<details><summary>Example (Arena)</summary>
<p>
```go
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())
}
}
```
#### Output
```
15
70
hello
world
```
</p>
</details>
## Index
- [func New\(a allocator.Allocator, options ...BatchAllocatorOption\) allocator.Allocator](<#New>)
- [type BatchAllocator](<#BatchAllocator>)
- [type BatchAllocatorOption](<#BatchAllocatorOption>)
- [func WithBucketSize\(size int\) BatchAllocatorOption](<#WithBucketSize>)
<a name="New"></a>
## func [New](<https://github.com/joetifa2003/mm-go/blob/master/batchallocator/batch.go#L59>)
```go
func New(a allocator.Allocator, options ...BatchAllocatorOption) allocator.Allocator
```
New creates a new BatchAllocator and applies optional configuration using BatchAllocatorOption
<a name="BatchAllocator"></a>
## type [BatchAllocator](<https://github.com/joetifa2003/mm-go/blob/master/batchallocator/batch.go#L42-L46>)
BatchAllocator manages a collection of memory buckets to optimize small allocations
```go
type BatchAllocator struct {
// contains filtered or unexported fields
}
```
<a name="BatchAllocatorOption"></a>
## type [BatchAllocatorOption](<https://github.com/joetifa2003/mm-go/blob/master/batchallocator/batch.go#L48>)
```go
type BatchAllocatorOption func(alloc *BatchAllocator)
```
<a name="WithBucketSize"></a>
### func [WithBucketSize](<https://github.com/joetifa2003/mm-go/blob/master/batchallocator/batch.go#L52>)
```go
func WithBucketSize(size int) BatchAllocatorOption
```
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.
<details><summary>Example</summary>
<p>
```go
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.
```
</p>
</details>
# hashmap
```go
import "github.com/joetifa2003/mm-go/hashmap"
```
<details><summary>Example</summary>
<p>
```go
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
```
</p>
</details>
## 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>)
<a name="Hashmap"></a>
## type [Hashmap](<https://github.com/joetifa2003/mm-go/blob/master/hashmap/hashmap.go#L15-L20>)
Hashmap Manually managed hashmap,
```go
type Hashmap[K comparable, V any] struct {
// contains filtered or unexported fields
}
```
<a name="New"></a>
### func [New](<https://github.com/joetifa2003/mm-go/blob/master/hashmap/hashmap.go#L28>)
```go
func New[K comparable, V any](alloc allocator.Allocator) *Hashmap[K, V]
```
New creates a new Hashmap with key of type K and value of type V
<a name="Hashmap[K, V].Delete"></a>Frequently Asked Questions
What is mm-go?
mm-go is a Performance library for the Go programming language. Generic manual memory management for golang
How do I install mm-go?
Install mm-go with the Go module system using `go get joetifa2003/mm-go`. Check the repository for the current installation instructions.
What category does mm-go belong to?
mm-go is listed under Performance, specifically Performance.