From 67dfed8b9b2b838504375e98e1731b388ce57034 Mon Sep 17 00:00:00 2001 From: "qiheng.zhou" Date: Thu, 17 Oct 2024 16:44:17 +0800 Subject: [PATCH] feat: new ring --- container/ring/ring.go | 123 +++++++++++++++++++++ container/ring/ring_test.go | 213 ++++++++++++++++++++++++++++++++++++ 2 files changed, 336 insertions(+) create mode 100644 container/ring/ring.go create mode 100644 container/ring/ring_test.go diff --git a/container/ring/ring.go b/container/ring/ring.go new file mode 100644 index 0000000..54eca1c --- /dev/null +++ b/container/ring/ring.go @@ -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 +} diff --git a/container/ring/ring_test.go b/container/ring/ring_test.go new file mode 100644 index 0000000..9f48223 --- /dev/null +++ b/container/ring/ring_test.go @@ -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() + } +}