diff --git a/alloc/benchmark_test.go b/alloc/benchmark_test.go deleted file mode 100644 index 94e78d7..0000000 --- a/alloc/benchmark_test.go +++ /dev/null @@ -1,168 +0,0 @@ -//nolint:ineffassign,staticcheck,wastedassign -package alloc - -import ( - "fmt" - "io" - "testing" - "unsafe" -) - -const ( - length = 1000 - elements = 10 -) - -func BenchmarkSliceOfByteSlices(b *testing.B) { - b.StopTimer() - b.ResetTimer() - - buffer := make([]byte, length*elements) - buffers := make([][]byte, 0, length) - for i := range cap(buffers) { - buffers = append(buffers, buffer[i*elements:(i+1)*elements]) - } - - var buf []byte - - b.StartTimer() - for range b.N { - for i := 0; i < length; i += 10 { - buf = buffers[i] - buf = buffers[i+1] - buf = buffers[i+2] - buf = buffers[i+3] - buf = buffers[i+4] - buf = buffers[i+5] - buf = buffers[i+6] - buf = buffers[i+7] - buf = buffers[i+8] - buf = buffers[i+9] - } - } - b.StopTimer() - - _, _ = fmt.Fprint(io.Discard, buf) -} - -func BenchmarkSliceOfBytePointers(b *testing.B) { - b.StopTimer() - b.ResetTimer() - - buffer := make([]byte, length*elements) - buffers := make([]*byte, 0, length) - for i := range cap(buffers) { - buffers = append(buffers, &buffer[i*elements]) - } - - var buf *byte - - b.StartTimer() - for range b.N { - for i := 0; i < length; i += 10 { - buf = buffers[i] - buf = buffers[i+1] - buf = buffers[i+2] - buf = buffers[i+3] - buf = buffers[i+4] - buf = buffers[i+5] - buf = buffers[i+6] - buf = buffers[i+7] - buf = buffers[i+8] - buf = buffers[i+9] - } - } - b.StopTimer() - - _, _ = fmt.Fprint(io.Discard, buf) -} - -func BenchmarkSliceOfUnsafePointers(b *testing.B) { - b.StopTimer() - b.ResetTimer() - - buffer := make([]byte, length*elements) - buffers := make([]unsafe.Pointer, 0, length) - for i := range cap(buffers) { - buffers = append(buffers, unsafe.Pointer(&buffer[i*elements])) - } - - var buf unsafe.Pointer - - b.StartTimer() - for range b.N { - for i := 0; i < length; i += 10 { - buf = buffers[i] - buf = buffers[i+1] - buf = buffers[i+2] - buf = buffers[i+3] - buf = buffers[i+4] - buf = buffers[i+5] - buf = buffers[i+6] - buf = buffers[i+7] - buf = buffers[i+8] - buf = buffers[i+9] - } - } - b.StopTimer() - - _, _ = fmt.Fprint(io.Discard, buf) -} - -func BenchmarkUintPtr(b *testing.B) { - b.StopTimer() - b.ResetTimer() - - buffer := make([]byte, length*elements) - bufferP := uintptr(unsafe.Pointer(&buffer[0])) - - var buf uintptr - - b.StartTimer() - for range b.N { - for i := uintptr(0); i < length; i += 10 { - buf = bufferP + i*elements - buf = bufferP + (i+1)*elements - buf = bufferP + (i+2)*elements - buf = bufferP + (i+3)*elements - buf = bufferP + (i+4)*elements - buf = bufferP + (i+5)*elements - buf = bufferP + (i+6)*elements - buf = bufferP + (i+7)*elements - buf = bufferP + (i+8)*elements - buf = bufferP + (i+9)*elements - } - } - b.StopTimer() - - _, _ = fmt.Fprint(io.Discard, buf) -} - -func BenchmarkUnsafePointer(b *testing.B) { - b.StopTimer() - b.ResetTimer() - - buffer := make([]byte, length*elements) - bufferP := unsafe.Pointer(&buffer[0]) - - var buf unsafe.Pointer - - b.StartTimer() - for range b.N { - for i := uintptr(0); i < length; i += 10 { - buf = unsafe.Add(bufferP, i*elements) - buf = unsafe.Add(bufferP, (i+1)*elements) - buf = unsafe.Add(bufferP, (i+2)*elements) - buf = unsafe.Add(bufferP, (i+3)*elements) - buf = unsafe.Add(bufferP, (i+4)*elements) - buf = unsafe.Add(bufferP, (i+5)*elements) - buf = unsafe.Add(bufferP, (i+6)*elements) - buf = unsafe.Add(bufferP, (i+7)*elements) - buf = unsafe.Add(bufferP, (i+8)*elements) - buf = unsafe.Add(bufferP, (i+9)*elements) - } - } - b.StopTimer() - - _, _ = fmt.Fprint(io.Discard, buf) -} diff --git a/alloc/ring.go b/alloc/ring.go new file mode 100644 index 0000000..212af39 --- /dev/null +++ b/alloc/ring.go @@ -0,0 +1,51 @@ +package alloc + +import ( + "github.com/pkg/errors" +) + +func newRing[T any](capacity uint64) (*ring[T], []T) { + addresses := make([]T, capacity) + return &ring[T]{ + addresses: addresses, + capacity: capacity, + commitPtr: capacity, + }, addresses +} + +type ring[T any] struct { + addresses []T + + capacity uint64 + allocPtr, commitPtr, deallocPtr uint64 +} + +func (r *ring[T]) Allocate() (T, error) { + if r.allocPtr == r.commitPtr { + var t T + return t, errors.New("no free address to allocate") + } + if r.allocPtr == r.capacity { + r.allocPtr = 0 + } + a := r.addresses[r.allocPtr] + r.allocPtr++ + return a, nil +} + +func (r *ring[T]) Deallocate(item T) { + if r.deallocPtr == r.capacity { + r.deallocPtr = 0 + } + if r.deallocPtr == r.allocPtr { + // This is really critical because it means that we deallocated more than allocated. + panic("no space left in the ring for deallocation") + } + + r.addresses[r.deallocPtr] = item + r.deallocPtr++ +} + +func (r *ring[T]) Commit() { + r.commitPtr = r.deallocPtr - 1 +} diff --git a/alloc/ring_test.go b/alloc/ring_test.go new file mode 100644 index 0000000..479abc5 --- /dev/null +++ b/alloc/ring_test.go @@ -0,0 +1,278 @@ +package alloc + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +const ringCapacity = 10 + +func prepRing() *ring[int] { + r, addresses := newRing[int](ringCapacity) + for i := range addresses { + addresses[i] = i + } + return r +} + +func TestRingMaxAllocation(t *testing.T) { + requireT := require.New(t) + r := prepRing() + + for i := range ringCapacity { + item, err := r.Allocate() + requireT.NoError(err, i) + requireT.Equal(i, item) + } + + item, err := r.Allocate() + requireT.Error(err) + requireT.Equal(0, item) +} + +func TestRingAllocationDeallocationWithoutCommitFromTheBeginning(t *testing.T) { + requireT := require.New(t) + r := prepRing() + + for i := range ringCapacity { + item, err := r.Allocate() + requireT.NoError(err, i) + requireT.Equal(i, item) + + r.Deallocate(item) + } + + item, err := r.Allocate() + requireT.Error(err) + requireT.Equal(0, item) +} + +func TestRingAllocationDeallocationWithCommitFromTheBeginning(t *testing.T) { + requireT := require.New(t) + r := prepRing() + + for i := range ringCapacity { + item, err := r.Allocate() + requireT.NoError(err, i) + requireT.Equal(i, item) + + r.Deallocate(item) + } + + r.Commit() + + for range 10 { + for i := range ringCapacity - 1 { + item, err := r.Allocate() + requireT.NoError(err, i) + requireT.Equal(i, item) + + r.Deallocate(item) + } + + item, err := r.Allocate() + requireT.Error(err) + requireT.Equal(0, item) + + r.Commit() + + item, err = r.Allocate() + requireT.NoError(err) + requireT.Equal(ringCapacity-1, item) + + r.Deallocate(item) + r.Commit() + } +} + +func TestRingAllocationDeallocationWithoutCommitFromTheMiddle(t *testing.T) { + requireT := require.New(t) + r := prepRing() + + // Prepare + + for range ringCapacity / 2 { + item, err := r.Allocate() + requireT.NoError(err) + r.Deallocate(item) + } + + r.Commit() + + // Test + + for i := range ringCapacity - 1 { + item, err := r.Allocate() + requireT.NoError(err, i) + requireT.Equal((i+ringCapacity/2)%ringCapacity, item) + + r.Deallocate(item) + } + + item, err := r.Allocate() + requireT.Error(err) + requireT.Equal(0, item) +} + +func TestRingAllocationDeallocationWithCommitFromTheMiddle(t *testing.T) { + requireT := require.New(t) + r := prepRing() + + // Prepare + + for range ringCapacity / 2 { + item, err := r.Allocate() + requireT.NoError(err) + r.Deallocate(item) + } + + r.Commit() + + // Test + + for range 10 { + for i := range ringCapacity - 1 { + item, err := r.Allocate() + requireT.NoError(err, i) + requireT.Equal((i+ringCapacity/2)%ringCapacity, item) + + r.Deallocate(item) + } + + item, err := r.Allocate() + requireT.Error(err) + requireT.Equal(0, item) + + r.Commit() + + item, err = r.Allocate() + requireT.NoError(err) + requireT.Equal(ringCapacity/2-1, item) + + r.Deallocate(item) + r.Commit() + } +} + +func TestRingAllocationDeallocationWithoutCommitFromTheEnd(t *testing.T) { + requireT := require.New(t) + r := prepRing() + + // Prepare + + for range ringCapacity { + item, err := r.Allocate() + requireT.NoError(err) + r.Deallocate(item) + } + + r.Commit() + + // Test + + for i := range ringCapacity - 1 { + item, err := r.Allocate() + requireT.NoError(err, i) + requireT.Equal(i, item) + + r.Deallocate(item) + } + + item, err := r.Allocate() + requireT.Error(err) + requireT.Equal(0, item) +} + +func TestRingAllocationDeallocationWithCommitFromTheEnd(t *testing.T) { + requireT := require.New(t) + r := prepRing() + + // Prepare + + for range ringCapacity { + item, err := r.Allocate() + requireT.NoError(err) + r.Deallocate(item) + } + + r.Commit() + + // Test + + for range 10 { + for i := range ringCapacity - 1 { + item, err := r.Allocate() + requireT.NoError(err, i) + requireT.Equal((i+ringCapacity)%ringCapacity, item) + + r.Deallocate(item) + } + + item, err := r.Allocate() + requireT.Error(err) + requireT.Equal(0, item) + + r.Commit() + + item, err = r.Allocate() + requireT.NoError(err) + requireT.Equal(ringCapacity-1, item) + + r.Deallocate(item) + r.Commit() + } +} + +func TestRingAllocationDeallocationCommitOneByOne(t *testing.T) { + requireT := require.New(t) + r := prepRing() + + for i := range 10 * ringCapacity { + item, err := r.Allocate() + requireT.NoError(err, i) + requireT.Equal(i%ringCapacity, item) + + r.Deallocate(item) + r.Commit() + } +} + +func TestRingDeallocationPanicsIfNothingHasBeenAllocated(t *testing.T) { + requireT := require.New(t) + r := prepRing() + + requireT.Panics(func() { + r.Deallocate(100) + }) + requireT.Panics(func() { + r.Deallocate(101) + }) + + _, err := r.Allocate() + requireT.NoError(err) + + r.Deallocate(102) +} + +func TestRingDeallocationPanicsIfDeallocatingMoreThanAllocated(t *testing.T) { + requireT := require.New(t) + r := prepRing() + + _, err := r.Allocate() + requireT.NoError(err) + + r.Deallocate(100) + + requireT.Panics(func() { + r.Deallocate(101) + }) + requireT.Panics(func() { + r.Deallocate(102) + }) + + _, err = r.Allocate() + requireT.NoError(err) + + r.Deallocate(103) +}