Compare commits
No commits in common. "1e64e35ccb6ed8748c752ed0457c4f34d9f11cd8" and "bc751cc791bd665a029ccbc06661e3ec8d0a7593" have entirely different histories.
1e64e35ccb
...
bc751cc791
15 changed files with 68 additions and 850 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,2 +0,0 @@
|
||||||
.DS_Store
|
|
||||||
.idea/
|
|
||||||
13
assert.go
13
assert.go
|
|
@ -1,13 +0,0 @@
|
||||||
//go:build !XX_DISABLE_ASSERT
|
|
||||||
|
|
||||||
package xx
|
|
||||||
|
|
||||||
func Assert(cond bool) {
|
|
||||||
if !cond {
|
|
||||||
panic("assertion failed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Unreachable() {
|
|
||||||
panic("unreachable")
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
//go:build XX_DISABLE_ASSERT
|
|
||||||
|
|
||||||
package xx
|
|
||||||
|
|
||||||
func Assert(cond bool) {}
|
|
||||||
|
|
||||||
func Unreachable() {}
|
|
||||||
74
mem/mem.go
74
mem/mem.go
|
|
@ -1,74 +0,0 @@
|
||||||
package mem
|
|
||||||
|
|
||||||
import (
|
|
||||||
"unsafe"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SizeOf returns the size (in bytes) of the given type.
|
|
||||||
//
|
|
||||||
// Not to be confused with [unsafe.Sizeof] which returns the size of a type via an expression.
|
|
||||||
func SizeOf[T any]() uintptr {
|
|
||||||
var zero T
|
|
||||||
return unsafe.Sizeof(zero)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AlignOf returns the alignment (in bytes) of the given type.
|
|
||||||
//
|
|
||||||
// Not to be confused with [unsafe.AlignOf] which returns the alignment of a type via an expression.
|
|
||||||
func AlignOf[T any]() uintptr {
|
|
||||||
var zero T
|
|
||||||
return unsafe.Alignof(zero)
|
|
||||||
}
|
|
||||||
|
|
||||||
// BitCast performs a bit conversion between two types of the same size.
|
|
||||||
//
|
|
||||||
// BitCast panics if the sizes of the types differ.
|
|
||||||
func BitCast[TOut any, TIn any](value *TIn) TOut {
|
|
||||||
if SizeOf[TOut]() != SizeOf[TIn]() {
|
|
||||||
panic("bitcast: sizes of types must match")
|
|
||||||
}
|
|
||||||
return *((*TOut)(unsafe.Pointer(value)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy copies size number of bytes from src into dst.
|
|
||||||
//
|
|
||||||
// Returns dst.
|
|
||||||
func Copy(dst, src unsafe.Pointer, size uintptr) unsafe.Pointer {
|
|
||||||
copy(unsafe.Slice((*byte)(dst), size), unsafe.Slice((*byte)(src), size))
|
|
||||||
return dst
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear overwrites 'count' number of bytes in 'dst' with a particular value.
|
|
||||||
//
|
|
||||||
// Returns dst.
|
|
||||||
func Clear(dst unsafe.Pointer, value byte, count uintptr) unsafe.Pointer {
|
|
||||||
b := (*byte)(dst)
|
|
||||||
for range count {
|
|
||||||
*b = value
|
|
||||||
b = (*byte)(unsafe.Add(dst, 1))
|
|
||||||
}
|
|
||||||
return dst
|
|
||||||
}
|
|
||||||
|
|
||||||
// Zero overwrites 'count' number of bytes in 'dst' with zeros.
|
|
||||||
//
|
|
||||||
// Returns dst.
|
|
||||||
func Zero(dst unsafe.Pointer, count uintptr) unsafe.Pointer {
|
|
||||||
return Clear(dst, 0, count)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AlignForward returns an address align to the next power-of-two alignment.
|
|
||||||
func AlignForward(address uintptr, alignment uintptr) uintptr {
|
|
||||||
if alignment == 0 || (alignment&(alignment-1)) != 0 {
|
|
||||||
panic("alignforward: alignment must be a power of two")
|
|
||||||
}
|
|
||||||
return (address + alignment - 1) &^ (alignment - 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AlignBackward returns an address align to the previous power-of-two alignment.
|
|
||||||
func AlignBackward(address uintptr, alignment uintptr) uintptr {
|
|
||||||
if alignment == 0 || (alignment&(alignment-1)) != 0 {
|
|
||||||
panic("alignbackward: alignment must be a power of two")
|
|
||||||
}
|
|
||||||
return address &^ (alignment - 1)
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
package mem_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.brut.systems/judah/xx/mem"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestBitCast(t *testing.T) {
|
|
||||||
a := uint32(0xFFFF_FFFF)
|
|
||||||
b := mem.BitCast[float32](&a)
|
|
||||||
c := mem.BitCast[uint32](&b)
|
|
||||||
if a != c {
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
|
|
||||||
v := uint8(0xFF)
|
|
||||||
d := mem.BitCast[int8](&v)
|
|
||||||
if d != -1 {
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
e := mem.BitCast[uint8](&d)
|
|
||||||
if e != 255 {
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -114,14 +114,12 @@ func (l Lane) Sync() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store sends 'value' to all lanes.
|
// Store sends 'value' to all lanes.
|
||||||
//
|
|
||||||
// Store can be called concurrently.
|
// Store can be called concurrently.
|
||||||
func (l Lane) Store(key, value any) {
|
func (l Lane) Store(key, value any) {
|
||||||
l.state.userdata.Store(key, value)
|
l.state.userdata.Store(key, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load fetches a named value, returning nil if it does not exist.
|
// Load fetches a named value, returning nil if it does not exist.
|
||||||
//
|
|
||||||
// Load can be called concurrently.
|
// Load can be called concurrently.
|
||||||
func (l Lane) Load(key any) any {
|
func (l Lane) Load(key any) any {
|
||||||
v, ok := l.state.userdata.Load(key)
|
v, ok := l.state.userdata.Load(key)
|
||||||
|
|
|
||||||
112
stable/array.go
112
stable/array.go
|
|
@ -1,112 +0,0 @@
|
||||||
package stable
|
|
||||||
|
|
||||||
import (
|
|
||||||
"iter"
|
|
||||||
)
|
|
||||||
|
|
||||||
const DefaultElementsPerBucket = 32
|
|
||||||
|
|
||||||
// Array is a resizable array whose values will never move in memory.
|
|
||||||
// This means it is safe to take a pointer to a value within the array
|
|
||||||
// while continuing to append to it.
|
|
||||||
type Array[T any] struct {
|
|
||||||
buckets []bucket[T]
|
|
||||||
last int
|
|
||||||
elements_per_bucket int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Array[T]) Init() {
|
|
||||||
s.InitWithCapacity(DefaultElementsPerBucket)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Array[T]) InitWithCapacity(elements_per_bucket int) {
|
|
||||||
if elements_per_bucket <= 0 {
|
|
||||||
elements_per_bucket = DefaultElementsPerBucket
|
|
||||||
}
|
|
||||||
|
|
||||||
s.elements_per_bucket = elements_per_bucket
|
|
||||||
|
|
||||||
s.buckets = s.buckets[:0]
|
|
||||||
s.buckets = append(s.buckets, make(bucket[T], 0, s.elements_per_bucket))
|
|
||||||
s.last = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Array[T]) Reset() {
|
|
||||||
s.buckets = s.buckets[:0]
|
|
||||||
s.last = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Array[T]) Append(value T) *T {
|
|
||||||
if len(s.buckets) == 0 {
|
|
||||||
s.Init()
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(s.buckets[s.last]) == cap(s.buckets[s.last]) {
|
|
||||||
s.buckets = append(s.buckets, make(bucket[T], 0, s.elements_per_bucket))
|
|
||||||
s.last += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
s.buckets[s.last] = append(s.buckets[s.last], value)
|
|
||||||
return &s.buckets[s.last][len(s.buckets[s.last])-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Array[T]) AppendMany(values ...T) (first *T) {
|
|
||||||
if len(values) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
first = s.Append(values[0])
|
|
||||||
|
|
||||||
if len(values) > 1 {
|
|
||||||
for _, v := range values[1:] {
|
|
||||||
s.Append(v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Array[T]) Get(index int) *T {
|
|
||||||
b := s.buckets[index/s.elements_per_bucket]
|
|
||||||
return &b[index%s.elements_per_bucket]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Array[T]) Set(index int, value T) {
|
|
||||||
*s.Get(index) = value
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Array[T]) Len() int {
|
|
||||||
return s.Cap() - (cap(s.buckets[s.last]) - len(s.buckets[s.last]))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Array[T]) Cap() int {
|
|
||||||
return len(s.buckets) * s.elements_per_bucket
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Array[T]) Pointers() iter.Seq2[int, *T] {
|
|
||||||
return func(yield func(int, *T) bool) {
|
|
||||||
for bi := range s.buckets {
|
|
||||||
startIdx := bi * s.elements_per_bucket
|
|
||||||
for i := range s.buckets[bi] {
|
|
||||||
if !yield(startIdx+i, &s.buckets[bi][i]) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Array[T]) Values() iter.Seq2[int, T] {
|
|
||||||
return func(yield func(int, T) bool) {
|
|
||||||
for bi, b := range s.buckets {
|
|
||||||
startIdx := bi * s.elements_per_bucket
|
|
||||||
for i := range b {
|
|
||||||
if !yield(startIdx+i, b[i]) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type bucket[T any] = []T
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
package stable_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"runtime"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.brut.systems/judah/xx/stable"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestArray_StableWithGC(t *testing.T) {
|
|
||||||
type valuewithptr struct {
|
|
||||||
value int
|
|
||||||
ptr *int
|
|
||||||
}
|
|
||||||
|
|
||||||
var arr stable.Array[valuewithptr]
|
|
||||||
aptr := arr.Append(valuewithptr{value: 10, ptr: nil})
|
|
||||||
bptr := arr.Append(valuewithptr{value: 20, ptr: &aptr.value})
|
|
||||||
|
|
||||||
const N = 1000
|
|
||||||
for i := range N {
|
|
||||||
arr.Append(valuewithptr{value: i})
|
|
||||||
runtime.GC()
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(t, arr.Get(0) == aptr)
|
|
||||||
expect(t, arr.Get(1) == bptr)
|
|
||||||
expect(t, arr.Len() == N+2, "len was %d", arr.Len())
|
|
||||||
expect(t, bptr.ptr != nil && bptr.value == 20)
|
|
||||||
expect(t, bptr.ptr == &aptr.value, "%p vs. %p", bptr.ptr, &aptr.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkArray_RandomAccess(b *testing.B) {
|
|
||||||
var arr stable.Array[int]
|
|
||||||
for i := range b.N {
|
|
||||||
arr.Append(i * i)
|
|
||||||
}
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := range b.N {
|
|
||||||
arr.Get(i % 10000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkArray_Append(b *testing.B) {
|
|
||||||
var arr stable.Array[int]
|
|
||||||
for i := range b.N {
|
|
||||||
arr.Append(i * i)
|
|
||||||
}
|
|
||||||
|
|
||||||
arr.Reset()
|
|
||||||
for i := range b.N {
|
|
||||||
arr.Append(i * i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkArray_Iteration(b *testing.B) {
|
|
||||||
var arr stable.Array[int]
|
|
||||||
for i := range b.N {
|
|
||||||
arr.Append(i * i)
|
|
||||||
}
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
|
|
||||||
sum := 0
|
|
||||||
for _, v := range arr.Values() {
|
|
||||||
sum += v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func expect(t *testing.T, cond bool, message ...any) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
if !cond {
|
|
||||||
if len(message) == 0 {
|
|
||||||
message = append(message, "assertion failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
str := message[0].(string)
|
|
||||||
t.Fatalf(str, message[1:]...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,83 +0,0 @@
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
@ -1,133 +0,0 @@
|
||||||
package stable_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.brut.systems/judah/xx/stable"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestPointer_AlignForward(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
offset uintptr
|
|
||||||
alignment uintptr
|
|
||||||
}{
|
|
||||||
{"align 8 bytes", 1, 8},
|
|
||||||
{"align 16 bytes", 3, 16},
|
|
||||||
{"align 32 bytes", 7, 32},
|
|
||||||
{"align 64 bytes", 15, 64},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
value := int32(789)
|
|
||||||
pinned := stable.PointerPin(&value)
|
|
||||||
defer pinned.Unpin()
|
|
||||||
|
|
||||||
// Add offset to misalign
|
|
||||||
misaligned := pinned.Add(tt.offset)
|
|
||||||
aligned := misaligned.AlignForward(tt.alignment)
|
|
||||||
|
|
||||||
// Check alignment
|
|
||||||
if aligned.Address()%tt.alignment != 0 {
|
|
||||||
t.Errorf("Address %d is not aligned to %d bytes", aligned.Address(), tt.alignment)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check it's forward aligned (greater or equal)
|
|
||||||
if aligned.Address() < misaligned.Address() {
|
|
||||||
t.Errorf("Forward aligned address %d should be >= original %d", aligned.Address(), misaligned.Address())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPointer_AlignBackward(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
offset uintptr
|
|
||||||
alignment uintptr
|
|
||||||
}{
|
|
||||||
{"align 8 bytes", 5, 8},
|
|
||||||
{"align 16 bytes", 10, 16},
|
|
||||||
{"align 32 bytes", 20, 32},
|
|
||||||
{"align 64 bytes", 40, 64},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
value := int32(321)
|
|
||||||
pinned := stable.PointerPin(&value)
|
|
||||||
defer pinned.Unpin()
|
|
||||||
|
|
||||||
// Add offset to misalign
|
|
||||||
misaligned := pinned.Add(tt.offset)
|
|
||||||
aligned := misaligned.AlignBackward(tt.alignment)
|
|
||||||
|
|
||||||
// Check alignment
|
|
||||||
if aligned.Address()%tt.alignment != 0 {
|
|
||||||
t.Errorf("Address %d is not aligned to %d bytes", aligned.Address(), tt.alignment)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check it's backward aligned (less or equal)
|
|
||||||
if aligned.Address() > misaligned.Address() {
|
|
||||||
t.Errorf("Backward aligned address %d should be <= original %d", aligned.Address(), misaligned.Address())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPointer_Nth(t *testing.T) {
|
|
||||||
// Test with int32 array
|
|
||||||
arr := []int32{10, 20, 30, 40, 50}
|
|
||||||
pinned := stable.PointerPin(&arr[0])
|
|
||||||
defer pinned.Unpin()
|
|
||||||
|
|
||||||
for i := 0; i < len(arr); i++ {
|
|
||||||
value := pinned.Nth(i)
|
|
||||||
if value != arr[i] {
|
|
||||||
t.Errorf("Index %d: expected %d, got %d", i, arr[i], value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPointer_NthFloat64(t *testing.T) {
|
|
||||||
// Test with a float64 array
|
|
||||||
arr := []float64{1.1, 2.2, 3.3, 4.4, 5.5}
|
|
||||||
pinned := stable.PointerPin(&arr[0])
|
|
||||||
defer pinned.Unpin()
|
|
||||||
|
|
||||||
for i := 0; i < len(arr); i++ {
|
|
||||||
value := pinned.Nth(i)
|
|
||||||
if value != arr[i] {
|
|
||||||
t.Errorf("Index %d: expected %f, got %f", i, arr[i], value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPointerArithmeticChain(t *testing.T) {
|
|
||||||
value := int32(888)
|
|
||||||
pinned := stable.PointerPin(&value)
|
|
||||||
defer pinned.Unpin()
|
|
||||||
|
|
||||||
// Test chaining operations
|
|
||||||
result := pinned.Add(16).Add(8).Sub(4)
|
|
||||||
expected := pinned.Address() + 16 + 8 - 4
|
|
||||||
|
|
||||||
if result.Address() != expected {
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
175
stable/xar.go
175
stable/xar.go
|
|
@ -1,175 +0,0 @@
|
||||||
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))
|
|
||||||
}
|
|
||||||
|
|
@ -1,104 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
49
utils.go
Normal file
49
utils.go
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
package xx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// New returns a newly allocated value with an initial value.
|
||||||
|
func New[T any](expr T) *T {
|
||||||
|
p := new(T)
|
||||||
|
*p = expr
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bitcast performs a bit conversion between two types of the same size.
|
||||||
|
//
|
||||||
|
// Bitcast panics if the sizes of the types differ.
|
||||||
|
func Bitcast[TOut any, TIn any](value TIn) TOut {
|
||||||
|
if SizeOf[TOut]() != SizeOf[TIn]() {
|
||||||
|
panic("bitcast: sizes of types must match")
|
||||||
|
}
|
||||||
|
return *((*TOut)(unsafe.Pointer(&value)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy copies src number of bytes into dst.
|
||||||
|
// Returns dst.
|
||||||
|
//
|
||||||
|
// Copy panics if src is smaller than dst.
|
||||||
|
func Copy[TDst any, TSrc any](dst *TDst, src *TSrc) *TDst {
|
||||||
|
if SizeOf[TSrc]() < SizeOf[TDst]() {
|
||||||
|
panic("copy: size of src must be >= dst")
|
||||||
|
}
|
||||||
|
MemCopy(unsafe.Pointer(dst), unsafe.Pointer(src), SizeOf[TDst]())
|
||||||
|
return dst
|
||||||
|
}
|
||||||
|
|
||||||
|
// MemCopy copies size number of bytes from src into dst.
|
||||||
|
// Returns dst.
|
||||||
|
func MemCopy(dst, src unsafe.Pointer, size uintptr) unsafe.Pointer {
|
||||||
|
copy(unsafe.Slice((*byte)(dst), size), unsafe.Slice((*byte)(src), size))
|
||||||
|
return dst
|
||||||
|
}
|
||||||
|
|
||||||
|
// SizeOf returns the size in bytes of the given type.
|
||||||
|
//
|
||||||
|
// Not to be confused with [unsafe.Sizeof] which returns the size of an expression.
|
||||||
|
func SizeOf[T any]() uintptr {
|
||||||
|
var zero T
|
||||||
|
return unsafe.Sizeof(zero)
|
||||||
|
}
|
||||||
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"git.brut.systems/judah/xx"
|
"git.brut.systems/judah/xx"
|
||||||
"git.brut.systems/judah/xx/mem"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNew(t *testing.T) {
|
func TestNew(t *testing.T) {
|
||||||
|
|
@ -14,7 +13,7 @@ func TestNew(t *testing.T) {
|
||||||
t.Fail()
|
t.Fail()
|
||||||
}
|
}
|
||||||
|
|
||||||
if unsafe.Sizeof(*a) != mem.SizeOf[uint32]() {
|
if unsafe.Sizeof(*a) != xx.SizeOf[uint32]() {
|
||||||
t.Fail()
|
t.Fail()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -43,3 +42,21 @@ func TestNew(t *testing.T) {
|
||||||
t.Fail()
|
t.Fail()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBitcast(t *testing.T) {
|
||||||
|
a := uint32(0xFFFF_FFFF)
|
||||||
|
b := xx.Bitcast[float32](a)
|
||||||
|
c := xx.Bitcast[uint32](b)
|
||||||
|
if a != c {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
d := xx.Bitcast[int8](uint8(0xFF))
|
||||||
|
if d != -1 {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
e := xx.Bitcast[uint8](d)
|
||||||
|
if e != 255 {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
35
xx.go
35
xx.go
|
|
@ -1,35 +0,0 @@
|
||||||
package xx
|
|
||||||
|
|
||||||
import (
|
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
"git.brut.systems/judah/xx/mem"
|
|
||||||
)
|
|
||||||
|
|
||||||
// New returns a newly allocated value with an initial value.
|
|
||||||
func New[T any](expr T) *T {
|
|
||||||
p := new(T)
|
|
||||||
*p = expr
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy copies src number of bytes into dst.
|
|
||||||
// Returns dst.
|
|
||||||
//
|
|
||||||
// Copy panics if src is smaller than dst.
|
|
||||||
func Copy[TDst any, TSrc any](dst *TDst, src *TSrc) *TDst {
|
|
||||||
if mem.SizeOf[TSrc]() < mem.SizeOf[TDst]() {
|
|
||||||
panic("copy: size of src must be >= dst")
|
|
||||||
}
|
|
||||||
mem.Copy(unsafe.Pointer(dst), unsafe.Pointer(src), mem.SizeOf[TDst]())
|
|
||||||
return dst
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clone returns a newly allocated shallow copy of the given value.
|
|
||||||
func Clone[T any](value *T) *T {
|
|
||||||
return Copy(new(T), value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func BoolUint(b bool) uint {
|
|
||||||
return uint(*(*uint8)(unsafe.Pointer(&b)))
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue