Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ring): new ring with less GC overhead #30

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()
}
}
Loading