diff --git a/stable/pointer.go b/stable/pointer.go new file mode 100644 index 0000000..f971f06 --- /dev/null +++ b/stable/pointer.go @@ -0,0 +1,83 @@ +package stable + +import ( + "runtime" + "unsafe" + + "git.brut.systems/judah/xx/mem" +) + +type Pointer[T any] struct { + base unsafe.Pointer + pinner runtime.Pinner +} + +func PointerPin[T any](ptr *T) (r Pointer[T]) { + r.pinner.Pin(ptr) + r.base = unsafe.Pointer(ptr) + return +} + +func PointerCast[TOut, TIn any](p Pointer[TIn]) Pointer[TOut] { + return Pointer[TOut]{ + base: unsafe.Pointer(p.base), + pinner: p.pinner, + } +} + +func (p Pointer[T]) Unpin() { + p.pinner.Unpin() + p.base = nil +} + +func (p Pointer[T]) Pointer() unsafe.Pointer { + return p.base +} + +func (p Pointer[T]) Address() uintptr { + return uintptr(p.base) +} + +func (p Pointer[T]) Nil() bool { + return p.base == nil +} + +func (p Pointer[T]) Add(amount uintptr) Pointer[T] { + return Pointer[T]{ + base: unsafe.Pointer(uintptr(p.base) + amount), + pinner: p.pinner, + } +} + +func (p Pointer[T]) Sub(amount uintptr) Pointer[T] { + return Pointer[T]{ + base: unsafe.Pointer(uintptr(p.base) - amount), + pinner: p.pinner, + } +} + +func (p Pointer[T]) AlignForward(alignment uintptr) Pointer[T] { + return Pointer[T]{ + base: unsafe.Pointer(mem.AlignForward(uintptr(p.base), alignment)), + pinner: p.pinner, + } +} + +func (p Pointer[T]) AlignBackward(alignment uintptr) Pointer[T] { + return Pointer[T]{ + base: unsafe.Pointer(mem.AlignBackward(uintptr(p.base), alignment)), + pinner: p.pinner, + } +} + +func (p Pointer[T]) Load() T { + return *(*T)(p.base) +} + +func (p Pointer[T]) Store(value T) { + *(*T)(p.base) = value +} + +func (p Pointer[T]) Nth(index int) T { + return p.Add(uintptr(index) * mem.SizeOf[T]()).Load() +} diff --git a/stable/pointer_test.go b/stable/pointer_test.go index 5ce6b6e..1587e8c 100644 --- a/stable/pointer_test.go +++ b/stable/pointer_test.go @@ -1,10 +1,12 @@ -package pointer +package stable_test import ( "testing" + + "git.brut.systems/judah/xx/stable" ) -func TestAlignForward(t *testing.T) { +func TestPointer_AlignForward(t *testing.T) { tests := []struct { name string offset uintptr @@ -19,7 +21,7 @@ func TestAlignForward(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { value := int32(789) - pinned := Pin(&value) + pinned := stable.PointerPin(&value) defer pinned.Unpin() // Add offset to misalign @@ -39,7 +41,7 @@ func TestAlignForward(t *testing.T) { } } -func TestAlignBackward(t *testing.T) { +func TestPointer_AlignBackward(t *testing.T) { tests := []struct { name string offset uintptr @@ -54,7 +56,7 @@ func TestAlignBackward(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { value := int32(321) - pinned := Pin(&value) + pinned := stable.PointerPin(&value) defer pinned.Unpin() // Add offset to misalign @@ -74,28 +76,28 @@ func TestAlignBackward(t *testing.T) { } } -func TestNth(t *testing.T) { +func TestPointer_Nth(t *testing.T) { // Test with int32 array arr := []int32{10, 20, 30, 40, 50} - pinned := Pin(&arr[0]) + pinned := stable.PointerPin(&arr[0]) defer pinned.Unpin() for i := 0; i < len(arr); i++ { - value := Nth[int32](pinned, i) + value := pinned.Nth(i) if value != arr[i] { t.Errorf("Index %d: expected %d, got %d", i, arr[i], value) } } } -func TestNthFloat64(t *testing.T) { +func TestPointer_NthFloat64(t *testing.T) { // Test with a float64 array arr := []float64{1.1, 2.2, 3.3, 4.4, 5.5} - pinned := Pin(&arr[0]) + pinned := stable.PointerPin(&arr[0]) defer pinned.Unpin() for i := 0; i < len(arr); i++ { - value := Nth[float64](pinned, i) + value := pinned.Nth(i) if value != arr[i] { t.Errorf("Index %d: expected %f, got %f", i, arr[i], value) } @@ -104,7 +106,7 @@ func TestNthFloat64(t *testing.T) { func TestPointerArithmeticChain(t *testing.T) { value := int32(888) - pinned := Pin(&value) + pinned := stable.PointerPin(&value) defer pinned.Unpin() // Test chaining operations @@ -115,3 +117,17 @@ func TestPointerArithmeticChain(t *testing.T) { t.Errorf("Expected address %d, got %d", expected, result.Address()) } } + +func TestPointer_Cast(t *testing.T) { + value := int32(123) + + i32 := stable.PointerPin(&value) + defer i32.Unpin() + + f32 := stable.PointerCast[float32](i32) + f32.Store(3.14) + + if value == 123 { + t.Errorf("Value should have been changed") + } +} diff --git a/stable/xar.go b/stable/xar.go index 485ffde..3163266 100644 --- a/stable/xar.go +++ b/stable/xar.go @@ -1 +1,175 @@ package stable + +import ( + "iter" + "math/bits" + "unsafe" + + "git.brut.systems/judah/xx" +) + +const ( + DefaultChunkSizeShift = 4 +) + +// Xar is an implementation of Andrew Reece's Xar (Exponential Array). +// +// See: https://www.youtube.com/watch?v=i-h95QIGchY +type Xar[T any] struct { + shift uint8 + count uint64 + chunks [][]T +} + +func (x *Xar[T]) Init() { + x.InitWithSize(DefaultChunkSizeShift) +} + +func (x *Xar[T]) InitWithSize(size_shift uint8) { + if len(x.chunks) != 0 { + x.Reset() + } + + x.shift = size_shift +} + +func (x *Xar[T]) Reset() { + for _, c := range x.chunks { + clear(c) + } + + x.count = 0 +} + +func (x *Xar[T]) Append(value T) *T { + if x.shift == 0 { + x.Init() + } + + chunk_idx, idx_in_chunk, chunk_cap := x.getChunk(x.count) + x.count += 1 + if chunk_idx >= uint64(len(x.chunks)) { + x.chunks = append(x.chunks, make([]T, chunk_cap)) + } + + slot := &x.chunks[chunk_idx][idx_in_chunk] + *slot = value + return slot +} + +func (x *Xar[T]) AppendMany(values ...T) *T { + if len(values) == 0 { + return nil + } + + first := x.Append(values[0]) + + if len(values) > 1 { + for _, v := range values[1:] { + x.Append(v) + } + } + + return first +} + +func (x *Xar[T]) Get(index int) *T { + chunk_idx, idx_in_chunk, _ := x.getChunk(uint64(index)) + return &x.chunks[chunk_idx][idx_in_chunk] +} + +func (x *Xar[T]) Set(index int, value T) { + *x.Get(index) = value +} + +func (x *Xar[T]) Remove(index int) { + x.Set(index, *x.Get(int(x.count - 1))) + x.count -= 1 +} + +func (x *Xar[T]) Len() int { + return int(x.count) +} + +func (x *Xar[T]) Cap() (l int) { + for _, c := range x.chunks { + l += cap(c) + } + return +} + +func (x *Xar[T]) Pointers() iter.Seq2[int, *T] { + return func(yield func(int, *T) bool) { + idx := -1 + for chunk_idx, idx_in_chunk := range x.iter() { + idx += 1 + if !yield(idx, &x.chunks[chunk_idx][idx_in_chunk]) { + break + } + } + } +} + +func (x *Xar[T]) Values() iter.Seq2[int, T] { + return func(yield func(int, T) bool) { + idx := -1 + for chunk_idx, idx_in_chunk := range x.iter() { + idx += 1 + if !yield(idx, x.chunks[chunk_idx][idx_in_chunk]) { + break + } + } + } +} + +func (x *Xar[T]) iter() iter.Seq2[uint64, uint64] { + return func(yield func(uint64, uint64) bool) { + chunk_size := 1 << x.shift + outer: + for chunk_idx := range x.chunks { + for idx_in_chunk := range chunk_size - 1 { + if uint64(chunk_idx+idx_in_chunk) >= uint64(x.count) { + break outer + } + + if !yield(uint64(chunk_idx), uint64(idx_in_chunk)) { + break outer + } + } + + chunk_size <<= xx.BoolUint(chunk_idx > 0) + } + } +} + +func (x *Xar[T]) getChunk(index uint64) (chunk_idx uint64, idx_in_chunk uint64, chunk_cap uint64) { + if true /* branchless */ { + var ( + i_shift = index >> x.shift + i_shift2 = i_shift != 0 + msb = msb64(i_shift | 1) + b = uint64(*(*uint8)(unsafe.Pointer(&i_shift2))) + ) + + chunk_idx = msb + b + idx_in_chunk = index - (b << (msb + uint64(x.shift))) + chunk_cap = 1 << (msb + uint64(x.shift)) + } else { + idx_in_chunk = index + chunk_cap = 1 << x.shift + + i_shift := index >> x.shift + if i_shift > 0 { + chunk_idx = msb64(i_shift | 1) + chunk_cap = 1 << (chunk_idx + uint64(x.shift)) + idx_in_chunk -= chunk_cap + chunk_idx += 1 + } + } + + return +} + +func msb64(x uint64) uint64 { + return uint64(63 - bits.LeadingZeros64(x)) +} diff --git a/stable/xar_test.go b/stable/xar_test.go index 485ffde..134a4e2 100644 --- a/stable/xar_test.go +++ b/stable/xar_test.go @@ -1 +1,104 @@ -package stable +package stable_test + +import ( + "runtime" + "testing" + + "git.brut.systems/judah/xx/stable" +) + +func TestXar_StableWithGC(t *testing.T) { + type valuewithptr struct { + value int + ptr *int + } + + var xar stable.Xar[valuewithptr] + xar.InitWithSize(8) + + aptr := xar.Append(valuewithptr{value: 10, ptr: nil}) + bptr := xar.Append(valuewithptr{value: 20, ptr: &aptr.value}) + + const N = 1000 + for i := range N { + xar.Append(valuewithptr{value: i}) + runtime.GC() + } + + expect(t, xar.Get(0) == aptr) + expect(t, xar.Get(1) == bptr) + expect(t, xar.Len() == N+2, "len was %d", xar.Len()) + expect(t, bptr.ptr != nil && bptr.value == 20) + expect(t, bptr.ptr == &aptr.value, "%p vs. %p", bptr.ptr, &aptr.value) +} + +func TestXar_ResetAndReuse(t *testing.T) { + var xar stable.Xar[int] + start := xar.Append(60) + xar.AppendMany(10, 20, 30, 40, 50) + + xar.Reset() + runtime.GC() + + expect(t, xar.Cap() != 0) + expect(t, xar.Len() == 0) + + xar.Append(0xFF) + xar.Append(0xFC) + xar.Append(0xFB) + + expect(t, xar.Get(0) == start) + expect(t, xar.Len() == 3) +} + +func TestXar_Iterators(t *testing.T) { + var xar stable.Xar[int] + xar.AppendMany(0, 1, 2, 3, 4, 5) + + iterations := 0 + for i, v := range xar.Values() { + iterations += 1 + expect(t, v == i, "v: %d, i: %d", v, i) + } + + expect(t, iterations == xar.Len()) +} + +func BenchmarkXar_Append(b *testing.B) { + var xar stable.Xar[int] + for i := range b.N { + xar.Append(i * i) + } + + xar.Reset() + for i := range b.N { + xar.Append(i * i) + } +} + +func BenchmarkXar_RandomAccess(b *testing.B) { + var xar stable.Xar[int] + for i := range b.N { + xar.Append(i * i) + } + + b.ResetTimer() + + for i := range b.N { + xar.Get(i % 10000) + } +} + +func BenchmarkXar_Iteration(b *testing.B) { + var xar stable.Xar[int] + for i := range b.N { + xar.Append(i * i) + } + + b.ResetTimer() + + sum := 0 + for _, v := range xar.Values() { + sum += v + } +}