diff --git a/README.md b/README.md index 1221692..9da9df3 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Also available: * `DefaultAllocator` - calls cgo for Malloc and Free * `ThresholdAllocator` - If the malloc size is <= a provided value, use one allocator. Otherwise, use the other. Allocations made above the threshold size are stored in a map to enable `Free`. You can use this with a `FixedBlockAllocator` to use the default allocator for large requests. You could also use several to set up a multi-tiered FBA, I suppose. -* `ArenaAllocator` - sits on top of another allocator. Exposes a FreeAll method which will free all memory allocated through the ArenaAllocator. +* `ArenaAllocator` - sits on top of another allocator. Exposes a FreeAll method which will free all memory allocated through the ArenaAllocator. ArenaAllocator is optimized for `FreeAll` and ordinary frees have a cost of O(N) ### Are these thread-safe? @@ -24,15 +24,26 @@ The DefaultAllocator is! And as slow as cgo is, it's still far faster than any l In terms of memory overhead, it's kind of bad! I use a lot of maps and slices to track allocated-but-not-freed data. In terms of speed: +Default cgo ``` BenchmarkDefaultTemporaryData -BenchmarkDefaultTemporaryData-16 12688918 96.51 ns/op -BenchmarkFBATemporaryData -BenchmarkFBATemporaryData-16 100000000 11.07 ns/op +BenchmarkDefaultTemporaryData-16 12792590 94.58 ns/op BenchmarkDefaultGrowShrink -BenchmarkDefaultGrowShrink-16 11640133 105.1 ns/op +BenchmarkDefaultGrowShrink-16 11286946 104.7 ns/op +``` + +Fixed Buffer +``` +BenchmarkFBATemporaryData +BenchmarkFBATemporaryData-16 123561244 9.714 ns/op BenchmarkFBAGrowShrink -BenchmarkFBAGrowShrink-16 61445820 34.88 ns/op +BenchmarkFBAGrowShrink-16 64682006 34.83 ns/op +``` + +Arena +``` +BenchmarkArenaTemporaryData +BenchmarkArenaTemporaryData-16 40963460 29.24 ns/op ``` "It's fine!" diff --git a/arena.go b/arena.go index 4714424..cb21305 100644 --- a/arena.go +++ b/arena.go @@ -7,38 +7,47 @@ import ( type ArenaAllocator struct { inner Allocator - allocations map[unsafe.Pointer]bool + allocations []unsafe.Pointer } func CreateArenaAllocator(inner Allocator) *ArenaAllocator { return &ArenaAllocator{ inner: inner, - - allocations: make(map[unsafe.Pointer]bool), + allocations: make([]unsafe.Pointer, 0, 1), } } func (a *ArenaAllocator) Malloc(size int) unsafe.Pointer { alloc := a.inner.Malloc(size) - a.allocations[alloc] = true + a.allocations = append(a.allocations, alloc) return alloc } func (a *ArenaAllocator) Free(ptr unsafe.Pointer) { - _, ok := a.allocations[ptr] - if !ok { + allocIndex := -1 + for i := 0; i < len(a.allocations); i++ { + if a.allocations[i] == ptr { + allocIndex = i + break + } + } + + if allocIndex < 0 { panic("arenaallocator: attempted to free a pointer which had not been allocated with this allocator") } - delete(a.allocations, ptr) + + newEnd := len(a.allocations)-1 + a.allocations[allocIndex] = a.allocations[newEnd] + a.allocations = a.allocations[:newEnd] a.inner.Free(ptr) } func (a *ArenaAllocator) FreeAll() { - for ptr := range a.allocations { - a.inner.Free(ptr) + for i := 0; i < len(a.allocations); i++ { + a.inner.Free(a.allocations[i]) } - a.allocations = make(map[unsafe.Pointer]bool) + a.allocations = nil } func (a *ArenaAllocator) Destroy() { diff --git a/bench_test.go b/bench_test.go index 6ed9b77..1087353 100644 --- a/bench_test.go +++ b/bench_test.go @@ -28,6 +28,21 @@ func BenchmarkFBATemporaryData(b *testing.B) { } } +func BenchmarkArenaTemporaryData(b *testing.B) { + alloc, err := CreateFixedBlockAllocator(&DefaultAllocator{}, 4096, 64, 8) + if err != nil { + b.FailNow() + } + defer alloc.Destroy() + + for i := 0; i < b.N/2; i++ { + arena := CreateArenaAllocator(alloc) + _ = arena.Malloc(64) + _ = arena.Malloc(64) + arena.FreeAll() + } +} + func BenchmarkDefaultGrowShrink(b *testing.B) { alloc := &DefaultAllocator{} defer alloc.Destroy()