Skip to content

Commit

Permalink
feat: new ring
Browse files Browse the repository at this point in the history
  • Loading branch information
ppzqh committed Oct 18, 2024
1 parent cb56ecb commit 67dfed8
Show file tree
Hide file tree
Showing 2 changed files with 336 additions and 0 deletions.
123 changes: 123 additions & 0 deletions container/ring/ring.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
* Copyright 2024 CloudWeGo Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package ring

// Ring is a GC friendly ring implementation.
// items are allocated by one malloc and cannot be resized. Item inside can be accesses and modified.
// type V must NOT contain pointer for performance concern.
type Ring[V any] struct {
items []Item[V]
}

// Item is the element stored in the Ring
type Item[V any] struct {
value V
idx int
}

func NewFromSlice[V any](vv []V) *Ring[V] {
r := &Ring[V]{}
r.items = make([]Item[V], len(vv))
for i := 0; i < len(vv); i++ {
r.items[i].value = vv[i]
r.items[i].idx = i
}
return r
}

// Head returns the first item.
func (r *Ring[V]) Head() *Item[V] {
if len(r.items) == 0 {
return nil
}
return &r.items[0]
}

// Get returns the ith item.
func (r *Ring[V]) Get(i int) (*Item[V], bool) {
if i < 0 || i >= len(r.items) {
return nil, false
}
return &r.items[i], true
}

// Next returns the next item of the ith item.
// Return the first(idx=0) item if i == len(r.items) - 1.
func (r *Ring[V]) Next(i int) (*Item[V], bool) {
if i < 0 || i >= len(r.items) {
return nil, false
}
if i == len(r.items)-1 {
return &r.items[0], true
}
return &r.items[i+1], true
}

// Prev returns the previous item of the ith item
// Return the last item(idx=len(items)-1) if i == 0.
func (r *Ring[V]) Prev(i int) (*Item[V], bool) {
if i < 0 || i >= len(r.items) {
return nil, false
}
if i == 0 {
return &r.items[len(r.items)-1], true
}
return &r.items[i-1], true
}

// Move returns the item moving n step from the ith item.
func (r *Ring[V]) Move(i, n int) (*Item[V], bool) {
if i < 0 || i >= len(r.items) {
return nil, false
}
var idx int
if n >= 0 {
idx = (i + n) % len(r.items)
} else {
idx = len(r.items) + (i+n)%len(r.items)
}
return &r.items[idx], true
}

// Do calls function f on each item of the ring in forward order.
func (r *Ring[V]) Do(f func(v *V)) {
for i := 0; i < len(r.items); i++ {
f(&r.items[i].value)
}
}

// Len returns the length of the ring.
func (r *Ring[V]) Len() int {
return len(r.items)
}

// Index returns the index of the item in the ring.
func (it *Item[V]) Index() int {
return it.idx
}

// Value returns the value of the item.
func (it *Item[V]) Value() V {
return it.value
}

// Pointer returns the pointer of the item.
// Use Pointer if you want to modify V.
// Do not reference to the pointer from other place.
func (it *Item[V]) Pointer() *V {
return &it.value
}
213 changes: 213 additions & 0 deletions container/ring/ring_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
/*
* Copyright 2024 CloudWeGo Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package ring

import (
"container/ring"
"fmt"
"math/rand"
"runtime"
"testing"

"github.com/stretchr/testify/assert"
)

type ringItem struct {
value int
}

func newRandomValue(n int) []int {
vs := make([]int, 0, n)
for i := 0; i < n; i++ {
vs = append(vs, rand.Intn(n))
}
return vs
}

func newRingItemSlice(vs []int) []ringItem {
items := make([]ringItem, 0, len(vs))
for i := 0; i < len(vs); i++ {
items = append(items, ringItem{value: vs[i]})
}
return items
}

func newStdRing(vs []ringItem) *ring.Ring {
r := ring.New(len(vs))
for i := 0; i < len(vs); i++ {
r.Value = &vs[i]
r = r.Next()
}
return r
}

func TestRing(t *testing.T) {
n := 100
vs := newRandomValue(n)

r := NewFromSlice(newRingItemSlice(vs))
// Get
for i := 0; i < n; i++ {
it, ok := r.Get(i)
assert.True(t, ok)
assert.Equal(t, vs[i], it.Value().value)
assert.Equal(t, vs[i], it.Pointer().value)
}
// Next
curr := r.Head()
h, _ := r.Get(0)
assert.Equal(t, curr, h)
for i := 0; i < n; i++ {
next, ok := r.Next(curr.Index())
assert.True(t, ok)
curr = next
}
assert.Equal(t, curr, h) // back to head
_, ok := r.Next(n + 1)
assert.False(t, ok)
// Prev
for i := 0; i < n; i++ {
prev, ok := r.Prev(curr.Index())
assert.True(t, ok)
curr = prev
}
assert.Equal(t, curr, h) // back to head
_, ok = r.Prev(n + 1)
assert.False(t, ok)
// Do
var (
expectedTotal int
actualTotal int
)
r.Do(func(v *ringItem) {
actualTotal += v.value
})
for i := 0; i < n; i++ {
expectedTotal += vs[i]
}
assert.Equal(t, expectedTotal, actualTotal)
// Modify
for i := 0; i < n; i++ {
it, ok := r.Get(i)
assert.True(t, ok)
newValue := i
it.Pointer().value = newValue
assert.Equal(t, newValue, it.Value().value)
}
}

func TestMove(t *testing.T) {
n := 100
vs := newRandomValue(n)
r := NewFromSlice(newRingItemSlice(vs))

realNext, _ := r.Move(98, 2)
expectedNext, _ := r.Get(0)
assert.Equal(t, realNext, expectedNext)

realNext, _ = r.Move(98, n+1)
expectedNext, _ = r.Get(99)
assert.Equal(t, realNext, expectedNext)

realNext, _ = r.Move(1, -2)
expectedNext, _ = r.Get(99)
assert.Equal(t, realNext, expectedNext)

realNext, _ = r.Move(1, -(2 + n))
expectedNext, _ = r.Get(99)
assert.Equal(t, realNext, expectedNext)
}

func BenchmarkNew(b *testing.B) {
nn := []int{100000, 400000}
for _, n := range nn {
vs := newRandomValue(n)

b.Run(fmt.Sprintf("std-keysize_n_%d", n), func(b *testing.B) {
b.ResetTimer()
for j := 0; j < b.N; j++ {
stdRing := newStdRing(newRingItemSlice(vs))
_ = stdRing
}
})
runtime.GC()

b.Run(fmt.Sprintf("new-keysize_n_%d", n), func(b *testing.B) {
b.ResetTimer()
for j := 0; j < b.N; j++ {
newRing := NewFromSlice(newRingItemSlice(vs))
_ = newRing
}
})
runtime.GC()
}
}

func BenchmarkDo(b *testing.B) {
nn := []int{10000, 40000}
for _, n := range nn {
vs := newRandomValue(n)
b.Run(fmt.Sprintf("std-keysize_n_%d", n), func(b *testing.B) {
b.ResetTimer()
stdRing := newStdRing(newRingItemSlice(vs))
for j := 0; j < b.N; j++ {
stdRing.Do(func(i any) {})
}
})
runtime.GC()

b.Run(fmt.Sprintf("new-keysize_n_%d", n), func(b *testing.B) {
b.ResetTimer()
newRing := NewFromSlice(newRingItemSlice(vs))
for j := 0; j < b.N; j++ {
newRing.Do(func(i *ringItem) {})
}
})
runtime.GC()
}
}

func BenchmarkGC(b *testing.B) {
nn := []int{100000, 400000}
for _, n := range nn {
vs := newRandomValue(n)

b.Run(fmt.Sprintf("std-keysize_n_%d", n), func(b *testing.B) {
stdRing := newStdRing(newRingItemSlice(vs))
b.ResetTimer()
for j := 0; j < b.N; j++ {
runtime.GC()
}
runtime.KeepAlive(stdRing)
stdRing = nil
_ = stdRing
})
runtime.GC()

b.Run(fmt.Sprintf("new-keysize_n_%d", n), func(b *testing.B) {
newRing := NewFromSlice(newRingItemSlice(vs))
b.ResetTimer()
for j := 0; j < b.N; j++ {
runtime.GC()
}
runtime.KeepAlive(newRing)
newRing = nil
_ = newRing
})
runtime.GC()
}
}

0 comments on commit 67dfed8

Please sign in to comment.