Skip to content

Commit

Permalink
Merge pull request #2 from joetifa2003/remove-cgo
Browse files Browse the repository at this point in the history
Remove cgo
  • Loading branch information
joetifa2003 authored Dec 4, 2022
2 parents 5c7c6a2 + f7a98b3 commit d6773d5
Show file tree
Hide file tree
Showing 10 changed files with 183 additions and 41 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ jobs:
- name: Test
run: go test ./...
- name: Benchmark
run: go test -bench . -count 5 > out.txt
run: go test -bench .
- name: Benchstat
run: benchstat out.txt
run: go test -bench . -count 5 > out.txt && benchstat out.txt
14 changes: 6 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,10 @@ and this is where mm-go comes in to play.
## Before using mm-go

- Golang doesn't have any way to manually allocate/free memory, so how does mm-go allocate/free?
It does so via cgo.
It does so via a custom allocator (see malloc.go) using direct system calls.
- 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)
- Lastly mm-go uses cgo for calloc/free and it's known that calling cgo has some overhead so try to minimize the calls to cgo (in hot loops for example)

## Installing

Expand All @@ -38,8 +37,7 @@ 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 and reduce calls to cgo by
preallocating memory with the specified chunk size.
Using this will simplify memory management.

```go
arena := mm.NewTypedArena[int](3)
Expand All @@ -63,7 +61,7 @@ assert.Equal(3, ints[1])

## Vector

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, use Vector instead
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 := mm.NewVector[int]()
Expand Down Expand Up @@ -111,7 +109,7 @@ assert.Equal(1, v.Pop())

## Alloc/Free

Alloc is a generic function that allocates T and returns a pointer to it that u can free later using Free
Alloc is a generic function that allocates T and returns a pointer to it that you can free later using Free

```go
ptr := mm.Alloc[int]() // allocates a single int and returns a ptr to it
Expand All @@ -133,7 +131,7 @@ defer mm.Free(ptr) // frees the struct (defer recommended to prevent leaks)

## AllocMany/FreeMany

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 u can free later using FreeMany
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
Expand Down Expand Up @@ -164,7 +162,7 @@ mm.FreeMany(allocated) // didn't use defer here because i'm doing a r
## Benchmarks

Check the test files and github actions for the benchmarks (linux, macos, windows).
mm-go can sometimes be 5-10 times faster, if you are not careful it will be slower!
mm-go can sometimes be 5-10 times faster.

```
Run benchstat out.txt
Expand Down
6 changes: 5 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ module github.com/joetifa2003/mm-go

go 1.19

require github.com/stretchr/testify v1.8.1
require (
github.com/edsrzf/mmap-go v1.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.0.0-20211216021012-1d35b9e2eb4e // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,6 +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/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ=
github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q=
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=
Expand All @@ -10,6 +12,8 @@ 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.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/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=
Expand Down
148 changes: 148 additions & 0 deletions malloc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package mm

import (
"math"
"sync"
"unsafe"

"github.com/edsrzf/mmap-go"
)

const pageSize = 4096

// Blocks allocated from MMap
type block struct {
data []byte // The actual data to allocate from
nextBlock *block // Pointer to the next block
}

// Metadata for each allocation
type metadata struct {
size int
free int8 // 0 free 1 not free
}

var sizeOfBlockStruct = unsafe.Sizeof(block{})
var sizeOfDataFieldInBlock = pageSize - sizeOfBlockStruct // The size of the data field in block struct (= pageSize - size of block struct)

var sizeOfMetaStruct = unsafe.Sizeof(metadata{})

var headBlock *block // the starting block to search

func createBlock(size int) *block {
// How many pages to allocate rounded up
pageMultiplier := int(math.Ceil(float64(size+int(sizeOfMetaStruct)) / float64(sizeOfDataFieldInBlock)))

// the size to allocate from MMap
blockSize := pageSize * pageMultiplier

// byte array from mmap
bytes, err := mmap.MapRegion(nil, blockSize, mmap.RDWR, mmap.ANON, 0)
if err != nil {
panic(err)
}

// casting the byte array to block struct
block := (*block)(unsafe.Pointer(&bytes[0]))
// block data is the remainder of the block struct size
block.data = unsafe.Slice(
(*byte)(unsafe.Pointer(&bytes[sizeOfBlockStruct])),
blockSize-int(sizeOfBlockStruct),
)

return block
}

// mutex to avoid race conditions (thread safety)
var mallocMutex sync.Mutex

func malloc(size int) unsafe.Pointer {
mallocMutex.Lock()
defer mallocMutex.Unlock()

// initialize
if headBlock == nil {
headBlock = createBlock(size)
}

// search for a free block
currentBlock := headBlock
for {
// get the metadata struct
metaPtr := (*metadata)(unsafe.Pointer(&currentBlock.data[0]))

// checks if the metaPtr is inside the data field in the block
for uintptr(unsafe.Pointer(metaPtr))+sizeOfMetaStruct+uintptr(size)-uintptr(unsafe.Pointer(&currentBlock.data[0])) <= uintptr(len(currentBlock.data)) {
if metaPtr.free == 0 {
if metaPtr.size == 0 {
// first time (free and size is zero)
metaPtr.size = size // sets the size to the wanted size
}

// checks if the size is greater than the wanted size
if metaPtr.size >= size {
metaPtr.free = 1 // make it not available

// zero out the memory
ptr := unsafe.Pointer(uintptr(unsafe.Pointer(metaPtr)) + sizeOfMetaStruct)
ptrData := unsafe.Slice(
(*byte)(ptr),
size,
)
for i := range ptrData {
ptrData[i] = 0
}

// finally returns the pointer to the data after the meta struct
return ptr
}
}

// if not found check the next meta struct
metaPtr = (*metadata)(
unsafe.Pointer(
uintptr(unsafe.Pointer(metaPtr)) +
sizeOfMetaStruct +
uintptr(metaPtr.size),
),
)
}

// creates another block if the current one have no next block
if currentBlock.nextBlock == nil {
currentBlock.nextBlock = createBlock(size)
}

// if the block is all occupied check the next block
currentBlock = currentBlock.nextBlock
}
}

// Free frees a pointer from Malloc
func free(ptr unsafe.Pointer) {
// gets the metadata struct
metaData := (*metadata)(unsafe.Pointer(uintptr(ptr) - sizeOfMetaStruct))

// makes it available
metaData.free = 0
}

func realloc(ptr unsafe.Pointer, size int) unsafe.Pointer {
oldMeta := (*metadata)(unsafe.Pointer(uintptr(ptr) - sizeOfMetaStruct))
oldData := unsafe.Slice(
(*byte)(ptr),
oldMeta.size,
)

newPtr := malloc(size)
newPtrData := unsafe.Slice(
(*byte)(newPtr),
size,
)

copy(newPtrData, oldData)

free(ptr)

return newPtr
}
25 changes: 5 additions & 20 deletions mm.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,43 +4,28 @@ import (
"unsafe"
)

// #include <stdlib.h>
import "C"

func c_calloc(n int, size int) unsafe.Pointer {
return C.calloc(C.size_t(n), C.size_t(size))
}

func c_free(ptr unsafe.Pointer) {
C.free(ptr)
}

func c_realloc(ptr unsafe.Pointer, newSize int) unsafe.Pointer {
return C.realloc(ptr, C.size_t(newSize))
}

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]() *T {
ptr := c_calloc(1, getSize[T]())
ptr := 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) {
c_free(unsafe.Pointer(ptr))
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 := c_calloc(n, getSize[T]())
ptr := malloc(getSize[T]() * n)
return unsafe.Slice(
(*T)(ptr),
n,
Expand All @@ -50,12 +35,12 @@ func AllocMany[T any](n int) []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
func FreeMany[T any](slice []T) {
c_free(unsafe.Pointer(&slice[0]))
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 := c_realloc(unsafe.Pointer(&slice[0]), getSize[T]()*newN)
ptr := realloc(unsafe.Pointer(&slice[0]), getSize[T]()*newN)
return unsafe.Slice(
(*T)(ptr),
newN,
Expand Down
1 change: 0 additions & 1 deletion mm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,6 @@ func TestAlloc(t *testing.T) {
ptr := Alloc[int]() // allocates a single int and returns a ptr to it
defer Free(ptr) // frees the int (defer recommended to prevent leaks)

assert.Equal(0, *ptr) // allocations are zeroed by default
*ptr = 15
assert.Equal(15, *ptr)
}
Expand Down
16 changes: 8 additions & 8 deletions arena.go → typed_arena.go
Original file line number Diff line number Diff line change
@@ -1,36 +1,36 @@
package mm

type chunk[T any] struct {
type typedChunk[T any] struct {
data []T
len int
}

func newChunk[T any](size int) *chunk[T] {
chunk := Alloc[chunk[T]]()
func newChunk[T any](size int) *typedChunk[T] {
chunk := Alloc[typedChunk[T]]()
chunk.data = AllocMany[T](size)

return chunk
}

func (c *chunk[T]) Alloc() *T {
func (c *typedChunk[T]) Alloc() *T {
c.len++
return &c.data[c.len-1]
}

func (c *chunk[T]) AllocMany(n int) []T {
func (c *typedChunk[T]) AllocMany(n int) []T {
oldLen := c.len
c.len += n
return c.data[oldLen:c.len]
}

func (c *chunk[T]) Free() {
func (c *typedChunk[T]) Free() {
FreeMany(c.data)
Free(c)
}

// TypedArena is a growable typed arena
type TypedArena[T any] struct {
chunks *Vector[*chunk[T]]
chunks *Vector[*typedChunk[T]]
chunkSize int
}

Expand All @@ -42,7 +42,7 @@ type TypedArena[T any] struct {
func NewTypedArena[T any](chunkSize int) *TypedArena[T] {
tArena := Alloc[TypedArena[T]]()
tArena.chunkSize = chunkSize
tArena.chunks = NewVector[*chunk[T]]()
tArena.chunks = NewVector[*typedChunk[T]]()

firstChunk := newChunk[T](chunkSize)
tArena.chunks.Push(firstChunk)
Expand Down
2 changes: 1 addition & 1 deletion arena_test.go → typed_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
func TestTypedArena(t *testing.T) {
assert := assert.New(t)

arena := NewTypedArena[int](3)
arena := NewTypedArena[int](2)
defer arena.Free()

int1 := arena.Alloc() // allocates 1 int from arena
Expand Down
Loading

0 comments on commit d6773d5

Please sign in to comment.