├── go.mod ├── .gitignore ├── pool.go ├── test_utils.go ├── bench_test.go ├── go.sum ├── LICENSE ├── README.md ├── allocator.go └── alloc_test.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/CannibalVox/cgoparam 2 | 3 | go 1.17 4 | 5 | require github.com/stretchr/testify v1.7.0 6 | 7 | require ( 8 | github.com/davecgh/go-spew v1.1.0 // indirect 9 | github.com/pmezard/go-difflib v1.0.0 // indirect 10 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | .idea/ 17 | -------------------------------------------------------------------------------- /pool.go: -------------------------------------------------------------------------------- 1 | package cgoparam 2 | 3 | import ( 4 | "runtime" 5 | "sync" 6 | ) 7 | 8 | const basePageSize = 4096 9 | const considerStandaloneSize = 1024 10 | 11 | var allocatorPool = sync.Pool{ 12 | New: func() interface{} { 13 | allocator := &Allocator{ 14 | basePageSize: basePageSize, 15 | considerStandaloneSize: considerStandaloneSize, 16 | 17 | basePages: []*allocatorPage{createPage(basePageSize)}, 18 | } 19 | runtime.SetFinalizer(allocator, func(a *Allocator) { 20 | a.basePages[0].Destroy() 21 | }) 22 | return allocator 23 | }, 24 | } 25 | 26 | func GetAlloc() *Allocator { 27 | return allocatorPool.Get().(*Allocator) 28 | } 29 | 30 | func ReturnAlloc(alloc *Allocator) { 31 | alloc.freeAll() 32 | allocatorPool.Put(alloc) 33 | } 34 | -------------------------------------------------------------------------------- /test_utils.go: -------------------------------------------------------------------------------- 1 | package cgoparam 2 | 3 | /* 4 | #include 5 | */ 6 | import "C" 7 | import "unsafe" 8 | 9 | // Test files can't call cgo so we have to put these as unexported non-test symbols 10 | // compiler will optimize these out, so no worries 11 | 12 | func allocAndDeallocSmallBatch() { 13 | xs := C.malloc(1) 14 | s := C.malloc(10) 15 | m := C.malloc(100) 16 | l := C.malloc(1000) 17 | C.free(l) 18 | C.free(m) 19 | C.free(s) 20 | C.free(xs) 21 | } 22 | 23 | func allocAndDeallocMultipage() { 24 | var allocs []unsafe.Pointer 25 | 26 | for i := 0; i < 10; i++ { 27 | allocs = append(allocs, C.malloc(1000)) 28 | } 29 | 30 | for i := 0; i < 10; i++ { 31 | C.free(allocs[i]) 32 | } 33 | } 34 | 35 | func callGoString(str unsafe.Pointer) string { 36 | return C.GoString((*C.char)(str)) 37 | } 38 | 39 | func callGoBytes(b unsafe.Pointer, len int) []byte { 40 | return C.GoBytes(b, C.int(len)) 41 | } 42 | -------------------------------------------------------------------------------- /bench_test.go: -------------------------------------------------------------------------------- 1 | package cgoparam 2 | 3 | import "testing" 4 | 5 | func BenchmarkRawCgoSmallBatch(b *testing.B) { 6 | for i := 0; i < b.N; i++ { 7 | allocAndDeallocSmallBatch() 8 | } 9 | } 10 | 11 | func cgoparamSmallBatch() { 12 | alloc := GetAlloc() 13 | defer ReturnAlloc(alloc) 14 | 15 | _ = alloc.Malloc(1) 16 | _ = alloc.Malloc(10) 17 | _ = alloc.Malloc(100) 18 | _ = alloc.Malloc(1000) 19 | } 20 | 21 | func BenchmarkCgoparamSmallBatch(b *testing.B) { 22 | for i := 0; i < b.N; i++ { 23 | cgoparamSmallBatch() 24 | } 25 | } 26 | 27 | func BenchmarkRawCgoMultipage(b *testing.B) { 28 | for i := 0; i < b.N; i++ { 29 | allocAndDeallocMultipage() 30 | } 31 | } 32 | 33 | func cgoparamMultipage() { 34 | alloc := GetAlloc() 35 | defer ReturnAlloc(alloc) 36 | 37 | for i := 0; i < 10; i++ { 38 | _ = alloc.Malloc(1000) 39 | } 40 | } 41 | 42 | func BenchmarkCgoparamMultipage(b *testing.B) { 43 | for i := 0; i < b.N; i++ { 44 | cgoparamMultipage() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 6 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 7 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 9 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 10 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 11 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Stephen Baynham 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cgoparam 2 | Fast, thread-safe arena allocators 3 | 4 | ### What is CgoParam? 5 | 6 | CgoParam is a refinement & specialization of [cgoalloc](https://github.com/CannibalVox/cgoalloc) - a general-purpose allocation proxying library for cgo. 7 | 8 | Cgoalloc offers paging fixed-buffer allocators that can reduce the dependence on C.malloc and C.free (as well as avoiding expensive cgocheckpointers by only passing C memory to C). It also allows various allocator types to be composed together to create interesting allocation & retainment strategies. 9 | 10 | However, cgoalloc requires allocators to be composed from several parts to be effective, and it's not quite as fast as it could be. The FixedBufferAllocator can perform an allocation and free in 10ns, but an arena allocator built on top of it can take up to 30ns. That's for incredibly small allocations, too, and it's slower when the arena allocator is built on top of a 3-part allocator to ensure it can handle memory of any size. cgoalloc structures are also not thread-safe. 11 | 12 | As a result of these limitations, cgoalloc can support any allocation and retainment strategy you might like. 13 | 14 | By contrast, cgoparam is built for a single purpose- temporary allocations of cgo parameter pointers, assigned just before a cgo call and freed just after. It uses a sync.Pool in order to make the library thread-safe (allocators are not thread-safe, but each thread can freely pull and return allocators), meaning that cgoparam can be an implementation detail of your cgo wrapper library- a thing your users don't have to worry about. 15 | 16 | For this role, cgo's performance is perfect: 17 | 18 | #### Small Batch Test 19 | 20 | Four allocations are assigned and then freed: 1 byte, 10 bytes, 100 bytes, and 1000 bytes 21 | 22 | ``` 23 | BenchmarkRawCgoSmallBatch-16 3185971 374.9 ns/op 24 | BenchmarkCgoparamSmallBatch-16 61446448 19.74 ns/op 25 | ``` 26 | 27 | #### Multi-Page Test 28 | 29 | Ten allocations of 1000 bytes are made and then freed 30 | 31 | ``` 32 | BenchmarkRawCgoMultipage-16 945380 1150 ns/op 33 | BenchmarkCgoparamMultipage-16 4181353 293.5 ns/op 34 | ``` 35 | 36 | Pages are 4096 bytes- any additional pages allocated during the life of a single allocator must both be malloc'd and freed, but if you stay within the 4096 byte limit, your performance will more closely resemble the first example: 5ns per alloc/free! 37 | 38 | ### Example 39 | 40 | ```go 41 | allocator := cgoparam.GetAlloc() 42 | defer cgoparam.ReturnAlloc(allocator) 43 | 44 | createInfo := (*C.VkBufferCreateInfo)(allocator.Malloc(C.sizeof_struct_VkBufferCreateInfo)) 45 | createInfo.sType = C.VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO 46 | createInfo.flags = 0 47 | createInfo.pName = allocator.CString(name) 48 | 49 | C.cgoCall(createInfo) 50 | ``` 51 | -------------------------------------------------------------------------------- /allocator.go: -------------------------------------------------------------------------------- 1 | package cgoparam 2 | 3 | /* 4 | #include 5 | */ 6 | import "C" 7 | import "unsafe" 8 | 9 | type allocatorPage struct { 10 | remainingSize int 11 | nextOffset int 12 | size int 13 | 14 | buffer unsafe.Pointer 15 | } 16 | 17 | func createPage(size int) *allocatorPage { 18 | ptr := C.malloc(C.size_t(size)) 19 | return &allocatorPage{ 20 | remainingSize: size, 21 | nextOffset: 0, 22 | size: size, 23 | 24 | buffer: ptr, 25 | } 26 | } 27 | 28 | func (p *allocatorPage) Destroy() { 29 | C.free(p.buffer) 30 | } 31 | 32 | func (p *allocatorPage) FreeAll() { 33 | p.remainingSize = p.size 34 | p.nextOffset = 0 35 | } 36 | 37 | func (p *allocatorPage) NextPtr(size int) unsafe.Pointer { 38 | if p.remainingSize < size { 39 | panic("attempted to allocate more memory from page than it had. this indicates a disastrous bug in cgoparam") 40 | } 41 | 42 | ptr := unsafe.Add(p.buffer, p.nextOffset) 43 | oldOffset := p.nextOffset 44 | 45 | p.nextOffset += size 46 | alignment := p.nextOffset % 8 47 | if alignment != 0 { 48 | p.nextOffset = p.nextOffset - (p.nextOffset % 8) + 8 49 | } 50 | 51 | p.remainingSize -= p.nextOffset - oldOffset 52 | 53 | return ptr 54 | } 55 | 56 | type Allocator struct { 57 | basePageSize int 58 | considerStandaloneSize int 59 | 60 | basePages []*allocatorPage 61 | standaloneAllocs []unsafe.Pointer 62 | } 63 | 64 | func (a *Allocator) Malloc(size int) unsafe.Pointer { 65 | currentPage := a.basePages[len(a.basePages)-1] 66 | if size > currentPage.remainingSize { 67 | if size >= a.considerStandaloneSize { 68 | buffer := C.malloc(C.size_t(size)) 69 | a.standaloneAllocs = append(a.standaloneAllocs, buffer) 70 | return buffer 71 | } 72 | 73 | newPage := createPage(a.basePageSize) 74 | a.basePages = append(a.basePages, newPage) 75 | return newPage.NextPtr(size) 76 | } 77 | 78 | return currentPage.NextPtr(size) 79 | } 80 | 81 | func (a *Allocator) CString(str string) unsafe.Pointer { 82 | strByteLen := len(str) + 1 83 | ptr := a.Malloc(strByteLen) 84 | ptrSlice := ([]byte)(unsafe.Slice((*byte)(ptr), strByteLen)) 85 | copy(ptrSlice, str) 86 | ptrSlice[strByteLen-1] = 0 87 | return ptr 88 | } 89 | 90 | func (a *Allocator) CBytes(b []byte) unsafe.Pointer { 91 | byteLen := len(b) 92 | ptr := a.Malloc(byteLen) 93 | ptrSlice := ([]byte)(unsafe.Slice((*byte)(ptr), byteLen)) 94 | copy(ptrSlice, b) 95 | return ptr 96 | } 97 | 98 | func (a *Allocator) freeAll() { 99 | basePageCount := len(a.basePages) 100 | standaloneCount := len(a.standaloneAllocs) 101 | 102 | a.basePages[0].FreeAll() 103 | for i := 1; i < basePageCount; i++ { 104 | a.basePages[i].Destroy() 105 | } 106 | a.basePages = a.basePages[:1] 107 | 108 | for i := 0; i < standaloneCount; i++ { 109 | C.free(a.standaloneAllocs[i]) 110 | } 111 | a.standaloneAllocs = a.standaloneAllocs[:0] 112 | } 113 | -------------------------------------------------------------------------------- /alloc_test.go: -------------------------------------------------------------------------------- 1 | package cgoparam 2 | 3 | import "testing" 4 | import "github.com/stretchr/testify/require" 5 | 6 | func TestBasicFunc(t *testing.T) { 7 | alloc := GetAlloc() 8 | require.NotNil(t, alloc) 9 | require.NotNil(t, alloc.Malloc(1)) 10 | require.NotNil(t, alloc.Malloc(100)) 11 | require.NotNil(t, alloc.Malloc(1000)) 12 | 13 | require.Len(t, alloc.basePages, 1) 14 | require.Equal(t, 1112, alloc.basePages[0].nextOffset) 15 | require.Equal(t, 4096, alloc.basePages[0].size) 16 | require.Equal(t, 2984, alloc.basePages[0].remainingSize) 17 | 18 | ReturnAlloc(alloc) 19 | 20 | require.Len(t, alloc.basePages, 1) 21 | require.Equal(t, 0, alloc.basePages[0].nextOffset) 22 | require.Equal(t, 4096, alloc.basePages[0].size) 23 | require.Equal(t, 4096, alloc.basePages[0].remainingSize) 24 | } 25 | 26 | func TestMultiPage(t *testing.T) { 27 | alloc := GetAlloc() 28 | require.NotNil(t, alloc) 29 | require.NotNil(t, alloc.Malloc(900)) 30 | require.NotNil(t, alloc.Malloc(900)) 31 | require.NotNil(t, alloc.Malloc(900)) 32 | require.NotNil(t, alloc.Malloc(900)) 33 | require.NotNil(t, alloc.Malloc(900)) 34 | 35 | require.Len(t, alloc.basePages, 2) 36 | require.Len(t, alloc.standaloneAllocs, 0) 37 | require.Equal(t, 3616, alloc.basePages[0].nextOffset) 38 | require.Equal(t, 4096, alloc.basePages[0].size) 39 | require.Equal(t, 480, alloc.basePages[0].remainingSize) 40 | 41 | require.Equal(t, 904, alloc.basePages[1].nextOffset) 42 | require.Equal(t, 4096, alloc.basePages[1].size) 43 | require.Equal(t, 3192, alloc.basePages[1].remainingSize) 44 | 45 | ReturnAlloc(alloc) 46 | 47 | require.Len(t, alloc.basePages, 1) 48 | require.Equal(t, 0, alloc.basePages[0].nextOffset) 49 | require.Equal(t, 4096, alloc.basePages[0].remainingSize) 50 | } 51 | 52 | func TestSlightlyTooBigStandalone(t *testing.T) { 53 | alloc := GetAlloc() 54 | require.NotNil(t, alloc) 55 | require.NotNil(t, alloc.Malloc(900)) 56 | require.NotNil(t, alloc.Malloc(900)) 57 | require.NotNil(t, alloc.Malloc(900)) 58 | require.NotNil(t, alloc.Malloc(900)) 59 | require.NotNil(t, alloc.Malloc(1200)) 60 | 61 | require.Len(t, alloc.basePages, 1) 62 | require.Len(t, alloc.standaloneAllocs, 1) 63 | require.Equal(t, 3616, alloc.basePages[0].nextOffset) 64 | require.Equal(t, 4096, alloc.basePages[0].size) 65 | require.Equal(t, 480, alloc.basePages[0].remainingSize) 66 | 67 | ReturnAlloc(alloc) 68 | 69 | require.Len(t, alloc.basePages, 1) 70 | require.Len(t, alloc.standaloneAllocs, 0) 71 | require.Equal(t, 0, alloc.basePages[0].nextOffset) 72 | require.Equal(t, 4096, alloc.basePages[0].remainingSize) 73 | } 74 | 75 | func TestCouldBeStandaloneButFit(t *testing.T) { 76 | alloc := GetAlloc() 77 | require.NotNil(t, alloc) 78 | require.NotNil(t, alloc.Malloc(900)) 79 | require.NotNil(t, alloc.Malloc(900)) 80 | require.NotNil(t, alloc.Malloc(900)) 81 | require.NotNil(t, alloc.Malloc(1200)) 82 | 83 | require.Len(t, alloc.basePages, 1) 84 | require.Len(t, alloc.standaloneAllocs, 0) 85 | require.Equal(t, 3912, alloc.basePages[0].nextOffset) 86 | require.Equal(t, 4096, alloc.basePages[0].size) 87 | require.Equal(t, 184, alloc.basePages[0].remainingSize) 88 | 89 | ReturnAlloc(alloc) 90 | 91 | require.Len(t, alloc.basePages, 1) 92 | require.Len(t, alloc.standaloneAllocs, 0) 93 | require.Equal(t, 0, alloc.basePages[0].nextOffset) 94 | require.Equal(t, 4096, alloc.basePages[0].remainingSize) 95 | } 96 | 97 | func TestCString(t *testing.T) { 98 | alloc := GetAlloc() 99 | defer ReturnAlloc(alloc) 100 | 101 | cStr := alloc.CString("WOW STRING") 102 | goStr := callGoString(cStr) 103 | require.Equal(t, "WOW STRING", goStr) 104 | } 105 | 106 | func TestCBytes(t *testing.T) { 107 | alloc := GetAlloc() 108 | defer ReturnAlloc(alloc) 109 | 110 | b := []byte("WOW STRING") 111 | 112 | cBytes := alloc.CBytes(b) 113 | goBytes := callGoBytes(cBytes, len(b)) 114 | require.Equal(t, []byte("WOW STRING"), goBytes) 115 | } 116 | --------------------------------------------------------------------------------