From 0edfa56aa100511aa024d02d5d0aa2dafb44336d Mon Sep 17 00:00:00 2001 From: lif0 <22912194+lif0@users.noreply.github.com> Date: Sat, 25 Oct 2025 22:21:02 +0300 Subject: [PATCH 01/10] [PKG-18] sync: add OrderedMap; --- utils/go.mod | 2 +- utils/internal/linked_list.go | 79 ++++++++++++++++++++++++++++++ utils/typex/ordered_map.go | 91 +++++++++++++++++++++++++++++++++++ 3 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 utils/internal/linked_list.go create mode 100644 utils/typex/ordered_map.go diff --git a/utils/go.mod b/utils/go.mod index 5b24e16..52d24e1 100644 --- a/utils/go.mod +++ b/utils/go.mod @@ -1,6 +1,6 @@ module github.com/lif0/pkg/utils -go 1.22 +go 1.23 require github.com/stretchr/testify v1.11.1 diff --git a/utils/internal/linked_list.go b/utils/internal/linked_list.go new file mode 100644 index 0000000..8644a68 --- /dev/null +++ b/utils/internal/linked_list.go @@ -0,0 +1,79 @@ +package internal + +type LinkedList[T any] struct { + size int + head *Node[T] + tail *Node[T] +} + +type Node[T any] struct { + Val T + Prev *Node[T] + Next *Node[T] +} + +// time: O(1); mem: O(1) +func (l *LinkedList[T]) Remove(node *Node[T]) { + if node == nil { + return + } + + // If a previous node exists, link it to the next one, skipping the current node. + if node.Prev != nil { + node.Prev.Next = node.Next + } + + // If a next node exists, set its previous pointer to the current node’s previous. + // Есть следующий, значит у следующего удаляем предыдущий(то есть себя) + if node.Next != nil { + node.Next.Prev = node.Prev + } + + // Если мы равны голове, значит, теперь голова равна следующему + if node == l.head { + l.head = l.head.Next + } + + // если мы хвост, значит хвост теперь равен предыдущему от текущего + if node == l.tail { + l.tail = l.tail.Prev + } + + l.size -= 1 +} + +// time: O(1); mem: O(1) +func (l *LinkedList[T]) Append(node *Node[T]) { + if l.tail == nil { + l.head = node + l.tail = node + } else { + l.tail.Next = node + node.Prev = l.tail + l.tail = node + } + + l.size += 1 +} + +// time: O(1); mem: O(1) +func (l *LinkedList[T]) GetHead() *Node[T] { + return l.head +} + +// time: O(1); mem: O(1) +func (l *LinkedList[T]) Len() int { + return l.size +} + +func (l *LinkedList[T]) Iter() func(func(int, T) bool) { + return func(yield func(int, T) bool) { + i := 0 + for n := l.head; n != nil; n = n.Next { + if !yield(i, n.Val) { + return + } + i++ + } + } +} diff --git a/utils/typex/ordered_map.go b/utils/typex/ordered_map.go new file mode 100644 index 0000000..76b9c29 --- /dev/null +++ b/utils/typex/ordered_map.go @@ -0,0 +1,91 @@ +package typex + +import ( + "github.com/lif0/pkg/utils/internal" +) + +type OrderedMap[K comparable, V any] struct { + dict map[K]*internal.Node[V] + list internal.LinkedList[V] +} + +func NewOrderedMap[K comparable, V any]() *OrderedMap[K, V] { + return &OrderedMap[K, V]{ + dict: make(map[K]*internal.Node[V]), + list: internal.LinkedList[V]{}, + } +} + +// Get ... +// time: O(1); mem: O(1) +func (this *OrderedMap[K, V]) Get(key K) (V, bool) { + if node, ok := this.dict[key]; ok { + return node.Val, true + } + + var zeroVal V + return zeroVal, false +} + +// Put ... +// time: O(1); mem: O(1) +func (this *OrderedMap[K, V]) Put(key K, value V) { + if node, ok := this.dict[key]; ok { + // this.removeNode(node) + // node.Val = value + // this.addNodeToTail(node) + + node.Val = value + } else { + node = &internal.Node[V]{Val: value} + this.list.Append(node) + this.dict[key] = node + } +} + +// Delete ... +// time: O(1); mem: O(1) +func (this *OrderedMap[K, V]) Delete(key K) { + if node, ok := this.dict[key]; ok { + this.list.Remove(node) + delete(this.dict, key) + } +} + +// GetValues ... +// time: O(N); mem: O(N) +func (this *OrderedMap[K, V]) GetValues() []V { + result := make([]V, this.list.Len()) + + if cap(result) == 0 { + return result + } + + if cap(result) == 1 { + result[0] = this.list.GetHead().Val + } + + for i, v := range this.list.Iter() { + result[i] = v + } + + return result +} + +// Delete built-in function deletes the element with the specified key +// (m[key]) from the OrderedMap. If m is nil or there is no such element, delete +// is a no-op. +func Delete[Type comparable, Type1 any](m *OrderedMap[Type, Type1], key Type) { + if m == nil { + return + } + + if m.list.Len() == 0 { + return + } + + if node, ok := m.dict[key]; ok { + m.list.Remove(node) + delete(m.dict, key) + } +} From 67eb09de77cc3f0b41ab47aeb83d905ae48e7536 Mon Sep 17 00:00:00 2001 From: lif0 <22912194+lif0@users.noreply.github.com> Date: Sat, 25 Oct 2025 22:21:28 +0300 Subject: [PATCH 02/10] [PKG-18] test: cover OrderedMap by tests; --- utils/internal/linked_list_test.go | 345 +++++++++++++++++++++++++++++ utils/typex/ordered_map_test.go | 290 ++++++++++++++++++++++++ 2 files changed, 635 insertions(+) create mode 100644 utils/internal/linked_list_test.go create mode 100644 utils/typex/ordered_map_test.go diff --git a/utils/internal/linked_list_test.go b/utils/internal/linked_list_test.go new file mode 100644 index 0000000..26fc222 --- /dev/null +++ b/utils/internal/linked_list_test.go @@ -0,0 +1,345 @@ +package internal_test + +import ( + "testing" + + "github.com/lif0/pkg/utils/internal" + "github.com/stretchr/testify/assert" +) + +func collect[T any](l *internal.LinkedList[T]) []T { + var out []T + for _, v := range l.Iter() { + out = append(out, v) + } + return out +} + +func Test_LinkedList_Append(t *testing.T) { + t.Parallel() + + t.Run("ok/append-to-empty", func(t *testing.T) { + t.Parallel() + var l internal.LinkedList[int] + + // act + l.Append(&internal.Node[int]{Val: 1}) + + // assert + assert.Equal(t, 1, l.Len()) + head := l.GetHead() + assert.NotNil(t, head) + assert.Equal(t, 1, head.Val) + assert.Nil(t, head.Prev) + assert.Nil(t, head.Next) + }) + + t.Run("ok/append-to-non-empty", func(t *testing.T) { + t.Parallel() + var l internal.LinkedList[int] + n1 := &internal.Node[int]{Val: 1} + n2 := &internal.Node[int]{Val: 2} + + // act + l.Append(n1) + l.Append(n2) + + // assert + assert.Equal(t, 2, l.Len()) + head := l.GetHead() + assert.Equal(t, 1, head.Val) + assert.Equal(t, 2, head.Next.Val) + assert.Equal(t, head, head.Next.Prev) + }) + + t.Run("bug/append-preserves-external-next", func(t *testing.T) { + t.Parallel() + var l internal.LinkedList[int] + externalTail := &internal.Node[int]{Val: 9} + n := &internal.Node[int]{Val: 1, Next: externalTail} + + // act + l.Append(n) + + // assert + assert.Equal(t, 1, l.Len()) + got := collect(&l) + // Итерация “видит” 2 узла, хотя Len()==1 — следствие того, что Append не сбрасывает node.Next + assert.Equal(t, []int{1, 9}, got) + }) +} + +func Test_LinkedList_Remove(t *testing.T) { + t.Parallel() + + t.Run("ok/remove-head", func(t *testing.T) { + t.Parallel() + var l internal.LinkedList[int] + n1 := &internal.Node[int]{Val: 1} + n2 := &internal.Node[int]{Val: 2} + n3 := &internal.Node[int]{Val: 3} + l.Append(n1) + l.Append(n2) + l.Append(n3) + + // act + l.Remove(n1) + + // assert + assert.Equal(t, 2, l.Len()) + head := l.GetHead() + assert.Equal(t, 2, head.Val) + assert.Nil(t, head.Prev) + assert.Equal(t, 3, head.Next.Val) + }) + + t.Run("ok/remove-tail", func(t *testing.T) { + t.Parallel() + var l internal.LinkedList[int] + n1 := &internal.Node[int]{Val: 1} + n2 := &internal.Node[int]{Val: 2} + l.Append(n1) + l.Append(n2) + + // act + l.Remove(n2) + + // assert + assert.Equal(t, 1, l.Len()) + head := l.GetHead() + assert.Equal(t, 1, head.Val) + assert.Nil(t, head.Next) + }) + + t.Run("ok/remove-middle", func(t *testing.T) { + t.Parallel() + var l internal.LinkedList[int] + n1 := &internal.Node[int]{Val: 1} + n2 := &internal.Node[int]{Val: 2} + n3 := &internal.Node[int]{Val: 3} + l.Append(n1) + l.Append(n2) + l.Append(n3) + + // act + l.Remove(n2) + + // assert + assert.Equal(t, 2, l.Len()) + head := l.GetHead() + assert.Equal(t, 1, head.Val) + assert.Equal(t, 3, head.Next.Val) + assert.Equal(t, head, head.Next.Prev) + }) + + t.Run("ok/remove-singleton", func(t *testing.T) { + t.Parallel() + var l internal.LinkedList[int] + n1 := &internal.Node[int]{Val: 1} + l.Append(n1) + + // act + l.Remove(n1) + + // assert + assert.Equal(t, 0, l.Len()) + assert.Nil(t, l.GetHead()) + }) + + t.Run("ok/remove-nil-noop", func(t *testing.T) { + t.Parallel() + var l internal.LinkedList[int] + l.Append(&internal.Node[int]{Val: 1}) + + // act + l.Remove(nil) + + // assert + assert.Equal(t, 1, l.Len()) + assert.Equal(t, []int{1}, collect(&l)) + }) + + t.Run("bug/remove-foreign-node-decrements-size", func(t *testing.T) { + t.Parallel() + var l internal.LinkedList[int] + l.Append(&internal.Node[int]{Val: 1}) + foreign := &internal.Node[int]{Val: 999} // не в списке + + // act + l.Remove(foreign) + + // assert + // Структура связей не изменилась (узел 1 всё ещё голова), но размер уменьшился с 1 до 0 + assert.Equal(t, []int{1}, collect(&l)) + assert.Equal(t, 0, l.Len()) + }) + + t.Run("bug/remove-twice-size-negative", func(t *testing.T) { + t.Parallel() + var l internal.LinkedList[int] + n := &internal.Node[int]{Val: 1} + l.Append(n) + + // act + l.Remove(n) + l.Remove(n) // повторно по тому же узлу + + // assert + assert.Equal(t, -1, l.Len()) + assert.Nil(t, l.GetHead()) + }) +} + +func Test_LinkedList_GetHead(t *testing.T) { + t.Parallel() + + t.Run("edge/empty", func(t *testing.T) { + t.Parallel() + var l internal.LinkedList[int] + + // act + h := l.GetHead() + + // assert + assert.Nil(t, h) + }) + + t.Run("ok/non-empty", func(t *testing.T) { + t.Parallel() + var l internal.LinkedList[int] + n1 := &internal.Node[int]{Val: 1} + n2 := &internal.Node[int]{Val: 2} + l.Append(n1) + l.Append(n2) + + // act + h := l.GetHead() + + // assert + assert.NotNil(t, h) + assert.Equal(t, 1, h.Val) + }) +} + +func Test_LinkedList_Len(t *testing.T) { + t.Parallel() + + t.Run("ok/increments-and-decrements", func(t *testing.T) { + t.Parallel() + var l internal.LinkedList[int] + n1 := &internal.Node[int]{Val: 1} + n2 := &internal.Node[int]{Val: 2} + n3 := &internal.Node[int]{Val: 3} + + // act + l.Append(n1) + l.Append(n2) + l.Append(n3) + l.Remove(n2) + + // assert + assert.Equal(t, 2, l.Len()) + assert.Equal(t, []int{1, 3}, collect(&l)) + }) +} + +func Test_LinkedList_Iter(t *testing.T) { + t.Parallel() + + t.Run("edge/empty", func(t *testing.T) { + t.Parallel() + var l internal.LinkedList[int] + iter := l.Iter() + + // arrange + calls := 0 + + // act + iter(func(i int, v int) bool { + calls++ + return true + }) + + // assert + assert.Equal(t, 0, calls) + }) + + t.Run("ok/full-iteration", func(t *testing.T) { + t.Parallel() + var l internal.LinkedList[string] + l.Append(&internal.Node[string]{Val: "a"}) + l.Append(&internal.Node[string]{Val: "b"}) + l.Append(&internal.Node[string]{Val: "c"}) + iter := l.Iter() + + // arrange + var gotIdx []int + var gotVal []string + + // act + iter(func(i int, v string) bool { + gotIdx = append(gotIdx, i) + gotVal = append(gotVal, v) + return true + }) + + // assert + assert.Equal(t, []int{0, 1, 2}, gotIdx) + assert.Equal(t, []string{"a", "b", "c"}, gotVal) + }) + + t.Run("ok/stop-immediately", func(t *testing.T) { + t.Parallel() + var l internal.LinkedList[int] + l.Append(&internal.Node[int]{Val: 10}) + l.Append(&internal.Node[int]{Val: 20}) + l.Append(&internal.Node[int]{Val: 30}) + iter := l.Iter() + + // arrange + var gotIdx []int + var gotVal []int + calls := 0 + + // act + iter(func(i int, v int) bool { + calls++ + gotIdx = append(gotIdx, i) + gotVal = append(gotVal, v) + return false + }) + + // assert + assert.Equal(t, 1, calls) + assert.Equal(t, []int{0}, gotIdx) + assert.Equal(t, []int{10}, gotVal) + }) + + t.Run("ok/stop-middle", func(t *testing.T) { + t.Parallel() + var l internal.LinkedList[int] + l.Append(&internal.Node[int]{Val: 1}) + l.Append(&internal.Node[int]{Val: 2}) + l.Append(&internal.Node[int]{Val: 3}) + l.Append(&internal.Node[int]{Val: 4}) + iter := l.Iter() + + // arrange + var gotIdx []int + var gotVal []int + calls := 0 + + // act + iter(func(i int, v int) bool { + calls++ + gotIdx = append(gotIdx, i) + gotVal = append(gotVal, v) + return i < 1 + }) + + // assert + assert.Equal(t, 2, calls) + assert.Equal(t, []int{0, 1}, gotIdx) + assert.Equal(t, []int{1, 2}, gotVal) + }) +} diff --git a/utils/typex/ordered_map_test.go b/utils/typex/ordered_map_test.go new file mode 100644 index 0000000..d2a0e3d --- /dev/null +++ b/utils/typex/ordered_map_test.go @@ -0,0 +1,290 @@ +package typex_test + +import ( + "testing" + + "github.com/lif0/pkg/utils/typex" + "github.com/stretchr/testify/assert" +) + +// // arrange +// func newOrderedMap[K comparable, V any]() *typex.OrderedMap[K, V] { +// var m typex.OrderedMap[K, V] +// initOrderedMapDict(&m) +// return &m +// } + +// // arrange +// func initOrderedMapDict[K comparable, V any](m *typex.OrderedMap[K, V]) { +// mv := reflect.ValueOf(m).Elem() +// dictField := mv.FieldByName("dict") +// // assert.NotNil(t, dictField, "dict field must exist") + +// // make map with the exact field type, without importing internal types +// newMap := reflect.MakeMapWithSize(dictField.Type(), 0) + +// // set unexported field via unsafe +// dictPtr := unsafe.Pointer(dictField.UnsafeAddr()) +// reflect.NewAt(dictField.Type(), dictPtr).Elem().Set(newMap) +// } + +func Test_OrderedMap_Get(t *testing.T) { + t.Parallel() + + t.Run("ok/after-put", func(t *testing.T) { + t.Parallel() + m := typex.NewOrderedMap[string, int]() + + // act + m.Put("x", 42) + got, ok := m.Get("x") + + // assert + assert.True(t, ok) + assert.Equal(t, 42, got) + }) + + t.Run("edge/unknown-key", func(t *testing.T) { + t.Parallel() + m := typex.NewOrderedMap[string, int]() + + // act + v, ok := m.Get("b") + + // assert + assert.False(t, ok) + assert.Equal(t, 0, v) + }) + + t.Run("edge/zero-value-safe-get", func(t *testing.T) { + t.Parallel() + var m typex.OrderedMap[string, int] + + // act + v, ok := m.Get("kek") + + // assert + assert.False(t, ok) // safe read from nil-map + assert.Equal(t, 0, v) + }) +} + +func Test_OrderedMap_Put(t *testing.T) { + t.Parallel() + + t.Run("panic/nil-dict", func(t *testing.T) { + t.Parallel() + var m typex.OrderedMap[int, int] + + // act + assert + assert.Panics(t, func() { + m.Put(1, 10) // запись в nil map внутри dict + }) + }) + + t.Run("ok/insert-order", func(t *testing.T) { + t.Parallel() + m := typex.NewOrderedMap[string, int]() + + // act + m.Put("a", 1) + m.Put("b", 2) + m.Put("c", 3) + + // assert + vals := m.GetValues() + assert.Equal(t, []int{1, 2, 3}, vals) + }) + + t.Run("ok/update-existing-preserve-order", func(t *testing.T) { + t.Parallel() + m := typex.NewOrderedMap[string, int]() + m.Put("a", 1) + m.Put("b", 2) + + // act + m.Put("a", 100) + + // assert + gotA, okA := m.Get("a") + assert.True(t, okA) + assert.Equal(t, 100, gotA) + + vals := m.GetValues() + // order var is not changed: [a, b] + assert.Equal(t, []int{100, 2}, vals) + }) +} + +func Test_OrderedMap_Delete(t *testing.T) { + t.Parallel() + + t.Run("ok/delete-existing", func(t *testing.T) { + t.Parallel() + m := typex.NewOrderedMap[string, int]() + m.Put("a", 1) + m.Put("b", 2) + m.Put("c", 3) + + // act + m.Delete("b") + + // assert + _, okB := m.Get("b") + assert.False(t, okB) + + vals := m.GetValues() + assert.Equal(t, []int{1, 3}, vals) + }) + + t.Run("ok/build-in-delete-not-existing", func(t *testing.T) { + t.Parallel() + m := typex.NewOrderedMap[string, int]() + m.Put("a", 1) + m.Put("c", 3) + + // act + typex.Delete(m, "b") + + // assert + _, okB := m.Get("b") + assert.False(t, okB) + + vals := m.GetValues() + assert.Equal(t, []int{1, 3}, vals) + }) + + t.Run("edge/nil-dict", func(t *testing.T) { + t.Parallel() + var m typex.OrderedMap[int, int] // nil + + // act: should't panic + m.Delete(1) + + // assert: the struct still empty + assert.Equal(t, 0, len(m.GetValues())) + }) +} + +func Test_OrderedMap_BuiltInDelete(t *testing.T) { + t.Parallel() + + t.Run("ok/delete", func(t *testing.T) { + t.Parallel() + m := typex.NewOrderedMap[string, int]() + m.Put("a", 1) + m.Put("b", 2) + m.Put("c", 3) + + // act + typex.Delete(m, "b") + + // assert + _, okB := m.Get("b") + assert.False(t, okB) + + vals := m.GetValues() + assert.Equal(t, []int{1, 3}, vals) + }) + + t.Run("ok/delete-not-existing", func(t *testing.T) { + t.Parallel() + m := typex.NewOrderedMap[string, int]() + m.Put("a", 1) + m.Put("c", 3) + + // act + typex.Delete(m, "b") + + // assert + _, okB := m.Get("b") + assert.False(t, okB) + + vals := m.GetValues() + assert.Equal(t, []int{1, 3}, vals) + }) + + t.Run("ok/nil", func(t *testing.T) { + t.Parallel() + var m *typex.OrderedMap[int, int] // nil + + // act: should't panic + typex.Delete(m, 1) + + // assert + assert.Nil(t, m) + }) + + t.Run("ok/empty", func(t *testing.T) { + t.Parallel() + m := typex.NewOrderedMap[int, int]() + + // act: should't panic + typex.Delete(m, 1) + + // assert: the struct still empty + assert.Equal(t, 0, len(m.GetValues())) + }) +} + +func Test_OrderedMap_GetValues(t *testing.T) { + t.Parallel() + + t.Run("edge/empty", func(t *testing.T) { + t.Parallel() + var m typex.OrderedMap[string, int] + + // act + vals := m.GetValues() + + // assert + assert.NotNil(t, vals) + assert.Equal(t, 0, len(vals)) + }) + + t.Run("ok/single", func(t *testing.T) { + t.Parallel() + m := typex.NewOrderedMap[string, int]() + m.Put("a", 7) + + // act + vals := m.GetValues() + + // assert + assert.Equal(t, 1, len(vals)) + assert.Equal(t, []int{7}, vals) + }) + + t.Run("ok/multiple", func(t *testing.T) { + t.Parallel() + m := typex.NewOrderedMap[int, string]() + m.Put(10, "x") + m.Put(20, "y") + m.Put(30, "z") + + // act + vals := m.GetValues() + + // assert + assert.Equal(t, []string{"x", "y", "z"}, vals) + }) +} + +func Test_NewOrderedMap(t *testing.T) { + t.Parallel() + + t.Run("ok/empty", func(t *testing.T) { + t.Parallel() + m := typex.NewOrderedMap[string, int]() + + // act + v, ok := m.Get("missing") + values := m.GetValues() + + // assert + assert.False(t, ok) + assert.Equal(t, 0, v) + assert.NotNil(t, values) + assert.Equal(t, 0, len(values)) + }) +} From 9278f4ff3c2e319bfb841f15085138c6f40bd35a Mon Sep 17 00:00:00 2001 From: lif0 <22912194+lif0@users.noreply.github.com> Date: Sat, 25 Oct 2025 22:21:51 +0300 Subject: [PATCH 03/10] [PKG-18] docs: update CHANGELOG & README; --- utils/CHANGELOG.md | 7 +++++++ utils/README.md | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/utils/CHANGELOG.md b/utils/CHANGELOG.md index a95758c..21cd2d3 100644 --- a/utils/CHANGELOG.md +++ b/utils/CHANGELOG.md @@ -9,6 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed ### Changed +## [v2.1.0] 2025-11-XX +### Added +- Set minimum go version as 1.23 +- Add struct OrderedMap[K]V; +### Fixed +### Changed + ## [v2.0.0] 2025-10-23 ### Added - Set minimum go version as 1.22 diff --git a/utils/README.md b/utils/README.md index cfbdd53..e65e46e 100644 --- a/utils/README.md +++ b/utils/README.md @@ -34,7 +34,7 @@ For full documentation, see [https://pkg.go.dev/github.com/lif0/pkg/utils](https ## ⚙️ Requirements -- **go 1.22 or higher** +- **go 1.23 or higher** ## 📦 Installation From dd01f6d0475c5b144441667979cbebc567ea709d Mon Sep 17 00:00:00 2001 From: lif0 <22912194+lif0@users.noreply.github.com> Date: Sat, 25 Oct 2025 23:50:33 +0300 Subject: [PATCH 04/10] [PKG-18] feat: add descriptions for each func; - add Iter() for range; --- concurrency/sync_object.go | 29 +++++++++++++++ utils/internal/linked_list.go | 10 ++++-- utils/internal/linked_list_test.go | 6 ++-- utils/typex/ordered_map.go | 58 +++++++++++++++++++++++++----- 4 files changed, 88 insertions(+), 15 deletions(-) create mode 100644 concurrency/sync_object.go diff --git a/concurrency/sync_object.go b/concurrency/sync_object.go new file mode 100644 index 0000000..af92329 --- /dev/null +++ b/concurrency/sync_object.go @@ -0,0 +1,29 @@ +package concurrency + +import "sync" + +type SyncValue[T any] struct { + v T + mu sync.Mutex +} + +func NewSyncValue[T any](value T) *SyncValue[T] { + return &SyncValue[T]{ + v: value, + mu: sync.Mutex{}, + } +} + +func (sv *SyncValue[T]) MutateValue(f func(v *T)) { + sv.mu.Lock() + defer sv.mu.Unlock() + + f(&sv.v) +} + +func (sv *SyncValue[T]) GetValue() T { + sv.mu.Lock() + defer sv.mu.Unlock() + + return sv.v +} diff --git a/utils/internal/linked_list.go b/utils/internal/linked_list.go index 8644a68..72e9522 100644 --- a/utils/internal/linked_list.go +++ b/utils/internal/linked_list.go @@ -12,6 +12,7 @@ type Node[T any] struct { Next *Node[T] } +// Remove ... // time: O(1); mem: O(1) func (l *LinkedList[T]) Remove(node *Node[T]) { if node == nil { @@ -24,17 +25,16 @@ func (l *LinkedList[T]) Remove(node *Node[T]) { } // If a next node exists, set its previous pointer to the current node’s previous. - // Есть следующий, значит у следующего удаляем предыдущий(то есть себя) if node.Next != nil { node.Next.Prev = node.Prev } - // Если мы равны голове, значит, теперь голова равна следующему + // if the node and the head is equal, set to head node's next. if node == l.head { l.head = l.head.Next } - // если мы хвост, значит хвост теперь равен предыдущему от текущего + // if the node and the tail is equal, set to tail node's previous. if node == l.tail { l.tail = l.tail.Prev } @@ -42,6 +42,7 @@ func (l *LinkedList[T]) Remove(node *Node[T]) { l.size -= 1 } +// Append ... // time: O(1); mem: O(1) func (l *LinkedList[T]) Append(node *Node[T]) { if l.tail == nil { @@ -56,16 +57,19 @@ func (l *LinkedList[T]) Append(node *Node[T]) { l.size += 1 } +// GetHead ... // time: O(1); mem: O(1) func (l *LinkedList[T]) GetHead() *Node[T] { return l.head } +// Len ... // time: O(1); mem: O(1) func (l *LinkedList[T]) Len() int { return l.size } +// Iter ... func (l *LinkedList[T]) Iter() func(func(int, T) bool) { return func(yield func(int, T) bool) { i := 0 diff --git a/utils/internal/linked_list_test.go b/utils/internal/linked_list_test.go index 26fc222..ab57f33 100644 --- a/utils/internal/linked_list_test.go +++ b/utils/internal/linked_list_test.go @@ -64,7 +64,6 @@ func Test_LinkedList_Append(t *testing.T) { // assert assert.Equal(t, 1, l.Len()) got := collect(&l) - // Итерация “видит” 2 узла, хотя Len()==1 — следствие того, что Append не сбрасывает node.Next assert.Equal(t, []int{1, 9}, got) }) } @@ -163,13 +162,12 @@ func Test_LinkedList_Remove(t *testing.T) { t.Parallel() var l internal.LinkedList[int] l.Append(&internal.Node[int]{Val: 1}) - foreign := &internal.Node[int]{Val: 999} // не в списке + foreign := &internal.Node[int]{Val: 999} // act l.Remove(foreign) // assert - // Структура связей не изменилась (узел 1 всё ещё голова), но размер уменьшился с 1 до 0 assert.Equal(t, []int{1}, collect(&l)) assert.Equal(t, 0, l.Len()) }) @@ -182,7 +180,7 @@ func Test_LinkedList_Remove(t *testing.T) { // act l.Remove(n) - l.Remove(n) // повторно по тому же узлу + l.Remove(n) // assert assert.Equal(t, -1, l.Len()) diff --git a/utils/typex/ordered_map.go b/utils/typex/ordered_map.go index 76b9c29..98f6a95 100644 --- a/utils/typex/ordered_map.go +++ b/utils/typex/ordered_map.go @@ -4,11 +4,18 @@ import ( "github.com/lif0/pkg/utils/internal" ) +// OrderedMap is a map[Type]Type1-like collection that preserves the order +// in which keys were inserted. It behaves like a regular map but +// allows deterministic iteration over its elements. +// +// OrderedMap is useful when both quick key-based access and +// predictable iteration order are desired. type OrderedMap[K comparable, V any] struct { dict map[K]*internal.Node[V] list internal.LinkedList[V] } +// NewOrderedMap returns a new empty OrderedMap. func NewOrderedMap[K comparable, V any]() *OrderedMap[K, V] { return &OrderedMap[K, V]{ dict: make(map[K]*internal.Node[V]), @@ -16,8 +23,12 @@ func NewOrderedMap[K comparable, V any]() *OrderedMap[K, V] { } } -// Get ... -// time: O(1); mem: O(1) +// Get retrieves the value stored under the given key. +// The second return value reports whether the key was present. +// +// Complexity: +// - time: O(1) +// - mem: O(1) func (this *OrderedMap[K, V]) Get(key K) (V, bool) { if node, ok := this.dict[key]; ok { return node.Val, true @@ -27,8 +38,13 @@ func (this *OrderedMap[K, V]) Get(key K) (V, bool) { return zeroVal, false } -// Put ... -// time: O(1); mem: O(1) +// Put sets the value for the given key. +// If the key already exists, its value is updated. +// Otherwise, a new entry is added to the end of the order. +// +// Complexity: +// - time: O(1) +// - mem: O(1) func (this *OrderedMap[K, V]) Put(key K, value V) { if node, ok := this.dict[key]; ok { // this.removeNode(node) @@ -43,8 +59,12 @@ func (this *OrderedMap[K, V]) Put(key K, value V) { } } -// Delete ... -// time: O(1); mem: O(1) +// Delete removes the element with the specified key. +// If the key does not exist, Delete does nothing. +// +// Complexity: +// - time: O(1) +// - mem: O(1) func (this *OrderedMap[K, V]) Delete(key K) { if node, ok := this.dict[key]; ok { this.list.Remove(node) @@ -52,8 +72,12 @@ func (this *OrderedMap[K, V]) Delete(key K) { } } -// GetValues ... -// time: O(N); mem: O(N) +// GetValues returns all values in insertion order. +// The returned slice has the same length as the number of elements. +// +// Complexity: +// - time: O(N) +// - mem: O(N) func (this *OrderedMap[K, V]) GetValues() []V { result := make([]V, this.list.Len()) @@ -72,9 +96,27 @@ func (this *OrderedMap[K, V]) GetValues() []V { return result } +// Iter iteration on map +// +// Example: +// +// m := NewOrderedMap[int, string]() +// for i, v := range m.Iter() { +// fmt.Println(i,v) +// } +func (this *OrderedMap[K, V]) Iter() func(func(int, V) bool) { + return this.list.Iter() +} + // Delete built-in function deletes the element with the specified key // (m[key]) from the OrderedMap. If m is nil or there is no such element, delete // is a no-op. +// +// Example: +// +// var om = NewOrderedMap[string, int]() +// om.Put("x", 1) +// typex.Delete(om, "x") func Delete[Type comparable, Type1 any](m *OrderedMap[Type, Type1], key Type) { if m == nil { return From 5ced58e234b2f4a285b4a9632aa70613f33301d0 Mon Sep 17 00:00:00 2001 From: lif0 <22912194+lif0@users.noreply.github.com> Date: Sat, 25 Oct 2025 23:51:41 +0300 Subject: [PATCH 05/10] [PKG-18] docs(README): add content about OrderedMap; --- README.md | 7 +++++ concurrency/sync_object.go | 29 -------------------- utils/README.md | 56 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 29 deletions(-) delete mode 100644 concurrency/sync_object.go diff --git a/README.md b/README.md index deadaba..ce714bd 100644 --- a/README.md +++ b/README.md @@ -150,3 +150,10 @@ To propose a new idea or package, please open an issue or discussion with: ## License [MIT](./LICENSE) + +--- + +## TODO common repo + +- [ ] Add linter in ci +- [ ] Add go sec in ci diff --git a/concurrency/sync_object.go b/concurrency/sync_object.go deleted file mode 100644 index af92329..0000000 --- a/concurrency/sync_object.go +++ /dev/null @@ -1,29 +0,0 @@ -package concurrency - -import "sync" - -type SyncValue[T any] struct { - v T - mu sync.Mutex -} - -func NewSyncValue[T any](value T) *SyncValue[T] { - return &SyncValue[T]{ - v: value, - mu: sync.Mutex{}, - } -} - -func (sv *SyncValue[T]) MutateValue(f func(v *T)) { - sv.mu.Lock() - defer sv.mu.Unlock() - - f(&sv.v) -} - -func (sv *SyncValue[T]) GetValue() T { - sv.mu.Lock() - defer sv.mu.Unlock() - - return sv.v -} diff --git a/utils/README.md b/utils/README.md index e65e46e..041478e 100644 --- a/utils/README.md +++ b/utils/README.md @@ -19,6 +19,8 @@ - [Examples](#-examples) - [Package: `errx`](#-package-errx) - [MultiError](#multierror) +- [Package: `typex`](#-package-typex) + - [OrderedMap](#multierror) - [Roadmap](#️-roadmap) - [License](#-license) @@ -169,6 +171,8 @@ size := EstimatePayloadOf(&arr) ## 📚 Package `errx` +Provide additional feature for error. + ### MultiError MultiError is a slice of errors implementing the error interface. @@ -210,6 +214,58 @@ for _, job := range jobs { return me.MaybeUnwrap() ``` +## 📚 Package `typex` + +Provide additional golang type. + +### OrderedMap + +OrderedMap is a map[Type]Type1-like collection that preserves the order in which keys were inserted. It behaves like a regular map but allows deterministic iteration over its elements. + +Useful: +Imagine you are making a closer or graceful shutdown lib, and you need to register/unregister some functions/service in it, and finally handle them in the order they were added. Use it structure. You are welcome🤗 + +The structure provide provice + +#### API + +| Func | Complexity (time / mem) | +| -------------------------------------------------------------- | ---------------------------- | +| `(m *OrderedMap[K, V]) Get(key K) (V, bool)` | O(1) / O(1) | +| `(m *OrderedMap[K, V]) Put(key K, value V)` | O(1) / O(1) | +| `(m *OrderedMap[K, V]) Delete(key K)` | O(1) / O(1) | +| `(m *OrderedMap[K, V]) GetValues() []V` | O(N) / O(N) | +| `(m *OrderedMap[K, V]) Iter() []V` | for i,v := range m.Iter() {} | +| `Delete[K comparable, V any](m *OrderedMap[K, V], key K)` | O(1) / O(1) | + + +#### Benchmark + +??? + +#### Examples + +```go +import "github.com/lif0/pkg/utils/typex" + + +func main() { + m := typex.NewOrderedMap[string, int]() + + m.Put("key", 10) + + v, ok := m.Get("key") // v = 10 + + m.Delete("key") // or build-in func typex.Delete(m, "key") + + for i,v := range m.Iter() { + fmt.Println(i,v) + } + + fmt.Println( len( m.GetValues() ) ) // will be print '0' +} +``` + ## 🗺️ Roadmap The future direction of this package is community-driven! Ideas and contributions are highly welcome. From 69598ec0c2a5f114a98d4c88c541a2b0ac3e47e9 Mon Sep 17 00:00:00 2001 From: lif0 <22912194+lif0@users.noreply.github.com> Date: Mon, 27 Oct 2025 00:03:20 +0300 Subject: [PATCH 06/10] [PKG-18] chore: rename typex to structx; --- concurrency/README.md | 2 +- sync/README.md | 2 +- utils/go.mod | 2 +- utils/internal/linked_list.go | 11 + utils/{typex => structx}/ordered_map.go | 13 +- utils/structx/ordered_map_bench_test.go | 435 +++++++++++++++++++ utils/{typex => structx}/ordered_map_test.go | 70 ++- 7 files changed, 483 insertions(+), 52 deletions(-) rename utils/{typex => structx}/ordered_map.go (92%) create mode 100644 utils/structx/ordered_map_bench_test.go rename utils/{typex => structx}/ordered_map_test.go (71%) diff --git a/concurrency/README.md b/concurrency/README.md index 2a8de26..1ebf507 100644 --- a/concurrency/README.md +++ b/concurrency/README.md @@ -35,7 +35,7 @@ For full documentation, see [https://pkg.go.dev/github.com/lif0/pkg/concurrency] ## ⚙️ Requirements -- **go 1.22 or higher** +- **go 1.19 or higher** ## 📦 Installation diff --git a/sync/README.md b/sync/README.md index 7ce0e14..9566dbb 100644 --- a/sync/README.md +++ b/sync/README.md @@ -30,7 +30,7 @@ For full documentation, see [https://pkg.go.dev/github.com/lif0/pkg/sync](https: ## ⚙️ Requirements -- **go 1.18 or higher** +- **go 1.19 or higher** ## 📦 Installation diff --git a/utils/go.mod b/utils/go.mod index 213d235..52d24e1 100644 --- a/utils/go.mod +++ b/utils/go.mod @@ -1,6 +1,6 @@ module github.com/lif0/pkg/utils -go 1.19 +go 1.23 require github.com/stretchr/testify v1.11.1 diff --git a/utils/internal/linked_list.go b/utils/internal/linked_list.go index 72e9522..dfba9a7 100644 --- a/utils/internal/linked_list.go +++ b/utils/internal/linked_list.go @@ -2,6 +2,7 @@ package internal type LinkedList[T any] struct { size int + pool []*Node[T] head *Node[T] tail *Node[T] } @@ -12,6 +13,16 @@ type Node[T any] struct { Next *Node[T] } +func NewLinkedList[T any](cap ...int) *LinkedList[T] { + // var llCap int + + // if len(cap) > 0 { + // llCap = cap[0] + // } + + return &LinkedList[T]{} +} + // Remove ... // time: O(1); mem: O(1) func (l *LinkedList[T]) Remove(node *Node[T]) { diff --git a/utils/typex/ordered_map.go b/utils/structx/ordered_map.go similarity index 92% rename from utils/typex/ordered_map.go rename to utils/structx/ordered_map.go index 98f6a95..c1cb7e1 100644 --- a/utils/typex/ordered_map.go +++ b/utils/structx/ordered_map.go @@ -1,4 +1,4 @@ -package typex +package structx import ( "github.com/lif0/pkg/utils/internal" @@ -16,9 +16,14 @@ type OrderedMap[K comparable, V any] struct { } // NewOrderedMap returns a new empty OrderedMap. -func NewOrderedMap[K comparable, V any]() *OrderedMap[K, V] { +func NewOrderedMap[K comparable, V any](cap ...int) *OrderedMap[K, V] { + var dictCap int + if len(cap) > 0 { + dictCap = cap[0] + } + return &OrderedMap[K, V]{ - dict: make(map[K]*internal.Node[V]), + dict: make(map[K]*internal.Node[V], dictCap), list: internal.LinkedList[V]{}, } } @@ -116,7 +121,7 @@ func (this *OrderedMap[K, V]) Iter() func(func(int, V) bool) { // // var om = NewOrderedMap[string, int]() // om.Put("x", 1) -// typex.Delete(om, "x") +// structx.Delete(om, "x") func Delete[Type comparable, Type1 any](m *OrderedMap[Type, Type1], key Type) { if m == nil { return diff --git a/utils/structx/ordered_map_bench_test.go b/utils/structx/ordered_map_bench_test.go new file mode 100644 index 0000000..dbe9d10 --- /dev/null +++ b/utils/structx/ordered_map_bench_test.go @@ -0,0 +1,435 @@ +package structx_test + +import ( + "math/rand" + "testing" + + "github.com/lif0/pkg/utils/structx" +) + +func makeInts(n int) ([]int, []int) { + keys := make([]int, n) + vals := make([]int, n) + r := rand.New(rand.NewSource(42)) + for i := 0; i < n; i++ { + keys[i] = i + vals[i] = r.Int() + } + return keys, vals +} + +func makeStrs(n int) ([]string, [][]string) { + keys := make([]string, n) + vals := make([][]string, n) + for i := 0; i < n; i++ { + keys[i] = "k_" + string(rune('a'+(i%26))) + "_" + itoa(i) + vals[i] = []string{"v", itoa(i)} + } + return keys, vals +} + +func makeStrEmpties(n int) ([]string, []struct{}) { + keys := make([]string, n) + vals := make([]struct{}, n) + for i := 0; i < n; i++ { + keys[i] = "k_" + itoa(i) + vals[i] = struct{}{} + } + return keys, vals +} + +func itoa(i int) string { + if i == 0 { + return "0" + } + buf := [20]byte{} + pos := len(buf) + neg := i < 0 + u := uint64(i) + if neg { + u = uint64(-i) + } + for u > 0 { + pos-- + buf[pos] = byte('0' + u%10) + u /= 10 + } + if neg { + pos-- + buf[pos] = '-' + } + return string(buf[pos:]) +} + +func Benchmark_OrderedMapIntInt(b *testing.B) { + const N = 10_000 + keys, vals := makeInts(N) + + b.Run("put/orderedMap", func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + for n := 0; n < b.N; n++ { + m := structx.NewOrderedMap[int, int](N) + for i := 0; i < N; i++ { + m.Put(keys[i], vals[i]) + } + } + }) + + b.Run("put/builtin", func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + for n := 0; n < b.N; n++ { + m := make(map[int]int, N) + for i := 0; i < N; i++ { + m[keys[i]] = vals[i] + } + } + }) + + b.Run("get_hit/orderedMap", func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + m := structx.NewOrderedMap[int, int](N) + for i := 0; i < N; i++ { + m.Put(keys[i], vals[i]) + } + b.ResetTimer() + var sink int + for n := 0; n < b.N; n++ { + for i := 0; i < N; i++ { + v, _ := m.Get(keys[i]) + sink ^= v + } + } + _ = sink + }) + + b.Run("get_hit/builtin", func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + m := make(map[int]int, N) + for i := 0; i < N; i++ { + m[keys[i]] = vals[i] + } + b.ResetTimer() + var sink int + for n := 0; n < b.N; n++ { + for i := 0; i < N; i++ { + sink ^= m[keys[i]] + } + } + _ = sink + }) + + b.Run("delete/orderedMap", func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + for n := 0; n < b.N; n++ { + m := structx.NewOrderedMap[int, int](N) + for i := 0; i < N; i++ { + m.Put(keys[i], vals[i]) + } + for i := 0; i < N; i++ { + m.Delete(keys[i]) + } + } + }) + + b.Run("delete/builtin", func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + for n := 0; n < b.N; n++ { + m := make(map[int]int, N) + for i := 0; i < N; i++ { + m[keys[i]] = vals[i] + } + for i := 0; i < N; i++ { + delete(m, keys[i]) + } + } + }) + + b.Run("iterate_values/orderedMap", func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + m := structx.NewOrderedMap[int, int](N) + for i := 0; i < N; i++ { + m.Put(keys[i], vals[i]) + } + b.ResetTimer() + var sink int + for n := 0; n < b.N; n++ { + for v := range m.GetValues() { + sink ^= v + } + } + _ = sink + }) + + b.Run("iterate_values/builtin_range", func(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + m := make(map[int]int, N) + for i := 0; i < N; i++ { + m[keys[i]] = vals[i] + } + b.ResetTimer() + var sink int + for n := 0; n < b.N; n++ { + for _, v := range m { + sink ^= v + } + } + _ = sink + }) +} + +func Benchmark_OrderedMap_vs_Builtin_StringSlice(b *testing.B) { + const N = 1_0000 + keys, vals := makeStrs(N) + + b.Run("put/ordered", func(b *testing.B) { + b.ReportAllocs() + for n := 0; n < b.N; n++ { + m := structx.NewOrderedMap[string, []string]() + for i := 0; i < N; i++ { + m.Put(keys[i], vals[i]) + } + } + }) + + b.Run("put/builtin", func(b *testing.B) { + b.ReportAllocs() + for n := 0; n < b.N; n++ { + m := make(map[string][]string, N) + for i := 0; i < N; i++ { + m[keys[i]] = vals[i] + } + } + }) + + b.Run("get_hit/ordered", func(b *testing.B) { + b.ReportAllocs() + m := structx.NewOrderedMap[string, []string]() + for i := 0; i < N; i++ { + m.Put(keys[i], vals[i]) + } + b.ResetTimer() + var sink int + for n := 0; n < b.N; n++ { + for i := 0; i < N; i++ { + v, _ := m.Get(keys[i]) + // потребляем длину, чтобы компилятор не выкинул + if len(v) > 0 { + sink ^= len(v[0]) + } + } + } + _ = sink + }) + + b.Run("get_hit/builtin", func(b *testing.B) { + b.ReportAllocs() + m := make(map[string][]string, N) + for i := 0; i < N; i++ { + m[keys[i]] = vals[i] + } + b.ResetTimer() + var sink int + for n := 0; n < b.N; n++ { + for i := 0; i < N; i++ { + v := m[keys[i]] + if len(v) > 0 { + sink ^= len(v[0]) + } + } + } + _ = sink + }) + + b.Run("delete/ordered", func(b *testing.B) { + b.ReportAllocs() + for n := 0; n < b.N; n++ { + m := structx.NewOrderedMap[string, []string]() + for i := 0; i < N; i++ { + m.Put(keys[i], vals[i]) + } + for i := 0; i < N; i++ { + m.Delete(keys[i]) + } + } + }) + + b.Run("delete/builtin", func(b *testing.B) { + b.ReportAllocs() + for n := 0; n < b.N; n++ { + m := make(map[string][]string, N) + for i := 0; i < N; i++ { + m[keys[i]] = vals[i] + } + for i := 0; i < N; i++ { + delete(m, keys[i]) + } + } + }) + + b.Run("iterate_values/ordered", func(b *testing.B) { + b.ReportAllocs() + m := structx.NewOrderedMap[string, []string]() + for i := 0; i < N; i++ { + m.Put(keys[i], vals[i]) + } + b.ResetTimer() + var sink int + for n := 0; n < b.N; n++ { + vs := m.GetValues() + for i := 0; i < len(vs); i++ { + if len(vs[i]) > 1 { + sink ^= len(vs[i][1]) + } + } + } + _ = sink + }) + + b.Run("iterate_values/builtin_range", func(b *testing.B) { + b.ReportAllocs() + m := make(map[string][]string, N) + for i := 0; i < N; i++ { + m[keys[i]] = vals[i] + } + b.ResetTimer() + var sink int + for n := 0; n < b.N; n++ { + for _, v := range m { + if len(v) > 1 { + sink ^= len(v[1]) + } + } + } + _ = sink + }) +} + +func Benchmark_OrderedMap_vs_Builtin_StringEmptyStruct(b *testing.B) { + const N = 1_0000 + keys, vals := makeStrEmpties(N) + + b.Run("put/ordered", func(b *testing.B) { + b.ReportAllocs() + for n := 0; n < b.N; n++ { + m := structx.NewOrderedMap[string, struct{}]() + for i := 0; i < N; i++ { + m.Put(keys[i], vals[i]) + } + } + }) + + b.Run("put/builtin", func(b *testing.B) { + b.ReportAllocs() + for n := 0; n < b.N; n++ { + m := make(map[string]struct{}, N) + for i := 0; i < N; i++ { + m[keys[i]] = vals[i] + } + } + }) + + b.Run("get_hit/ordered", func(b *testing.B) { + b.ReportAllocs() + m := structx.NewOrderedMap[string, struct{}]() + for i := 0; i < N; i++ { + m.Put(keys[i], vals[i]) + } + b.ResetTimer() + var sink int + for n := 0; n < b.N; n++ { + for i := 0; i < N; i++ { + _, ok := m.Get(keys[i]) + if ok { + sink ^= 1 + } + } + } + _ = sink + }) + + b.Run("get_hit/builtin", func(b *testing.B) { + b.ReportAllocs() + m := make(map[string]struct{}, N) + for i := 0; i < N; i++ { + m[keys[i]] = vals[i] + } + b.ResetTimer() + var sink int + for n := 0; n < b.N; n++ { + for i := 0; i < N; i++ { + _, ok := m[keys[i]] + if ok { + sink ^= 1 + } + } + } + _ = sink + }) + + b.Run("delete/ordered", func(b *testing.B) { + b.ReportAllocs() + for n := 0; n < b.N; n++ { + m := structx.NewOrderedMap[string, struct{}]() + for i := 0; i < N; i++ { + m.Put(keys[i], vals[i]) + } + for i := 0; i < N; i++ { + m.Delete(keys[i]) + } + } + }) + + b.Run("delete/builtin", func(b *testing.B) { + b.ReportAllocs() + for n := 0; n < b.N; n++ { + m := make(map[string]struct{}, N) + for i := 0; i < N; i++ { + m[keys[i]] = vals[i] + } + for i := 0; i < N; i++ { + delete(m, keys[i]) + } + } + }) + + b.Run("iterate_values/ordered", func(b *testing.B) { + b.ReportAllocs() + m := structx.NewOrderedMap[string, struct{}]() + for i := 0; i < N; i++ { + m.Put(keys[i], vals[i]) + } + b.ResetTimer() + var sink int + for n := 0; n < b.N; n++ { + vs := m.GetValues() + for i := 0; i < len(vs); i++ { + sink ^= 1 + } + } + _ = sink + }) + + b.Run("iterate_values/builtin_range", func(b *testing.B) { + b.ReportAllocs() + m := make(map[string]struct{}, N) + for i := 0; i < N; i++ { + m[keys[i]] = vals[i] + } + b.ResetTimer() + var sink int + for n := 0; n < b.N; n++ { + for range m { + sink ^= 1 + } + } + _ = sink + }) +} diff --git a/utils/typex/ordered_map_test.go b/utils/structx/ordered_map_test.go similarity index 71% rename from utils/typex/ordered_map_test.go rename to utils/structx/ordered_map_test.go index d2a0e3d..86a7111 100644 --- a/utils/typex/ordered_map_test.go +++ b/utils/structx/ordered_map_test.go @@ -1,39 +1,19 @@ -package typex_test +package structx_test import ( "testing" - "github.com/lif0/pkg/utils/typex" "github.com/stretchr/testify/assert" -) - -// // arrange -// func newOrderedMap[K comparable, V any]() *typex.OrderedMap[K, V] { -// var m typex.OrderedMap[K, V] -// initOrderedMapDict(&m) -// return &m -// } - -// // arrange -// func initOrderedMapDict[K comparable, V any](m *typex.OrderedMap[K, V]) { -// mv := reflect.ValueOf(m).Elem() -// dictField := mv.FieldByName("dict") -// // assert.NotNil(t, dictField, "dict field must exist") -// // make map with the exact field type, without importing internal types -// newMap := reflect.MakeMapWithSize(dictField.Type(), 0) - -// // set unexported field via unsafe -// dictPtr := unsafe.Pointer(dictField.UnsafeAddr()) -// reflect.NewAt(dictField.Type(), dictPtr).Elem().Set(newMap) -// } + "github.com/lif0/pkg/utils/structx" +) func Test_OrderedMap_Get(t *testing.T) { t.Parallel() t.Run("ok/after-put", func(t *testing.T) { t.Parallel() - m := typex.NewOrderedMap[string, int]() + m := structx.NewOrderedMap[string, int]() // act m.Put("x", 42) @@ -46,7 +26,7 @@ func Test_OrderedMap_Get(t *testing.T) { t.Run("edge/unknown-key", func(t *testing.T) { t.Parallel() - m := typex.NewOrderedMap[string, int]() + m := structx.NewOrderedMap[string, int]() // act v, ok := m.Get("b") @@ -58,7 +38,7 @@ func Test_OrderedMap_Get(t *testing.T) { t.Run("edge/zero-value-safe-get", func(t *testing.T) { t.Parallel() - var m typex.OrderedMap[string, int] + var m structx.OrderedMap[string, int] // act v, ok := m.Get("kek") @@ -74,7 +54,7 @@ func Test_OrderedMap_Put(t *testing.T) { t.Run("panic/nil-dict", func(t *testing.T) { t.Parallel() - var m typex.OrderedMap[int, int] + var m structx.OrderedMap[int, int] // act + assert assert.Panics(t, func() { @@ -84,7 +64,7 @@ func Test_OrderedMap_Put(t *testing.T) { t.Run("ok/insert-order", func(t *testing.T) { t.Parallel() - m := typex.NewOrderedMap[string, int]() + m := structx.NewOrderedMap[string, int]() // act m.Put("a", 1) @@ -98,7 +78,7 @@ func Test_OrderedMap_Put(t *testing.T) { t.Run("ok/update-existing-preserve-order", func(t *testing.T) { t.Parallel() - m := typex.NewOrderedMap[string, int]() + m := structx.NewOrderedMap[string, int]() m.Put("a", 1) m.Put("b", 2) @@ -121,7 +101,7 @@ func Test_OrderedMap_Delete(t *testing.T) { t.Run("ok/delete-existing", func(t *testing.T) { t.Parallel() - m := typex.NewOrderedMap[string, int]() + m := structx.NewOrderedMap[string, int]() m.Put("a", 1) m.Put("b", 2) m.Put("c", 3) @@ -139,12 +119,12 @@ func Test_OrderedMap_Delete(t *testing.T) { t.Run("ok/build-in-delete-not-existing", func(t *testing.T) { t.Parallel() - m := typex.NewOrderedMap[string, int]() + m := structx.NewOrderedMap[string, int]() m.Put("a", 1) m.Put("c", 3) // act - typex.Delete(m, "b") + structx.Delete(m, "b") // assert _, okB := m.Get("b") @@ -156,7 +136,7 @@ func Test_OrderedMap_Delete(t *testing.T) { t.Run("edge/nil-dict", func(t *testing.T) { t.Parallel() - var m typex.OrderedMap[int, int] // nil + var m structx.OrderedMap[int, int] // nil // act: should't panic m.Delete(1) @@ -171,13 +151,13 @@ func Test_OrderedMap_BuiltInDelete(t *testing.T) { t.Run("ok/delete", func(t *testing.T) { t.Parallel() - m := typex.NewOrderedMap[string, int]() + m := structx.NewOrderedMap[string, int]() m.Put("a", 1) m.Put("b", 2) m.Put("c", 3) // act - typex.Delete(m, "b") + structx.Delete(m, "b") // assert _, okB := m.Get("b") @@ -189,12 +169,12 @@ func Test_OrderedMap_BuiltInDelete(t *testing.T) { t.Run("ok/delete-not-existing", func(t *testing.T) { t.Parallel() - m := typex.NewOrderedMap[string, int]() + m := structx.NewOrderedMap[string, int]() m.Put("a", 1) m.Put("c", 3) // act - typex.Delete(m, "b") + structx.Delete(m, "b") // assert _, okB := m.Get("b") @@ -206,10 +186,10 @@ func Test_OrderedMap_BuiltInDelete(t *testing.T) { t.Run("ok/nil", func(t *testing.T) { t.Parallel() - var m *typex.OrderedMap[int, int] // nil + var m *structx.OrderedMap[int, int] // nil // act: should't panic - typex.Delete(m, 1) + structx.Delete(m, 1) // assert assert.Nil(t, m) @@ -217,10 +197,10 @@ func Test_OrderedMap_BuiltInDelete(t *testing.T) { t.Run("ok/empty", func(t *testing.T) { t.Parallel() - m := typex.NewOrderedMap[int, int]() + m := structx.NewOrderedMap[int, int]() // act: should't panic - typex.Delete(m, 1) + structx.Delete(m, 1) // assert: the struct still empty assert.Equal(t, 0, len(m.GetValues())) @@ -232,7 +212,7 @@ func Test_OrderedMap_GetValues(t *testing.T) { t.Run("edge/empty", func(t *testing.T) { t.Parallel() - var m typex.OrderedMap[string, int] + var m structx.OrderedMap[string, int] // act vals := m.GetValues() @@ -244,7 +224,7 @@ func Test_OrderedMap_GetValues(t *testing.T) { t.Run("ok/single", func(t *testing.T) { t.Parallel() - m := typex.NewOrderedMap[string, int]() + m := structx.NewOrderedMap[string, int]() m.Put("a", 7) // act @@ -257,7 +237,7 @@ func Test_OrderedMap_GetValues(t *testing.T) { t.Run("ok/multiple", func(t *testing.T) { t.Parallel() - m := typex.NewOrderedMap[int, string]() + m := structx.NewOrderedMap[int, string]() m.Put(10, "x") m.Put(20, "y") m.Put(30, "z") @@ -275,7 +255,7 @@ func Test_NewOrderedMap(t *testing.T) { t.Run("ok/empty", func(t *testing.T) { t.Parallel() - m := typex.NewOrderedMap[string, int]() + m := structx.NewOrderedMap[string, int]() // act v, ok := m.Get("missing") From ce225f7eb49d0f75fd582e0daa8bb1b9a99fe975 Mon Sep 17 00:00:00 2001 From: lif0 <22912194+lif0@users.noreply.github.com> Date: Wed, 29 Oct 2025 21:07:07 +0300 Subject: [PATCH 07/10] [PKG-18] chore: rename LinkedList to ObjectChain; --- .../{linked_list.go => object_chain.go} | 16 +++---- ...nked_list_test.go => object_chain_test.go} | 46 +++++++++---------- 2 files changed, 31 insertions(+), 31 deletions(-) rename utils/internal/{linked_list.go => object_chain.go} (78%) rename utils/internal/{linked_list_test.go => object_chain_test.go} (87%) diff --git a/utils/internal/linked_list.go b/utils/internal/object_chain.go similarity index 78% rename from utils/internal/linked_list.go rename to utils/internal/object_chain.go index dfba9a7..75aa7f5 100644 --- a/utils/internal/linked_list.go +++ b/utils/internal/object_chain.go @@ -1,6 +1,6 @@ package internal -type LinkedList[T any] struct { +type ObjectChain[T any] struct { size int pool []*Node[T] head *Node[T] @@ -13,19 +13,19 @@ type Node[T any] struct { Next *Node[T] } -func NewLinkedList[T any](cap ...int) *LinkedList[T] { +func NewLinkedList[T any](cap ...int) *ObjectChain[T] { // var llCap int // if len(cap) > 0 { // llCap = cap[0] // } - return &LinkedList[T]{} + return &ObjectChain[T]{} } // Remove ... // time: O(1); mem: O(1) -func (l *LinkedList[T]) Remove(node *Node[T]) { +func (l *ObjectChain[T]) Remove(node *Node[T]) { if node == nil { return } @@ -55,7 +55,7 @@ func (l *LinkedList[T]) Remove(node *Node[T]) { // Append ... // time: O(1); mem: O(1) -func (l *LinkedList[T]) Append(node *Node[T]) { +func (l *ObjectChain[T]) Append(node *Node[T]) { if l.tail == nil { l.head = node l.tail = node @@ -70,18 +70,18 @@ func (l *LinkedList[T]) Append(node *Node[T]) { // GetHead ... // time: O(1); mem: O(1) -func (l *LinkedList[T]) GetHead() *Node[T] { +func (l *ObjectChain[T]) GetHead() *Node[T] { return l.head } // Len ... // time: O(1); mem: O(1) -func (l *LinkedList[T]) Len() int { +func (l *ObjectChain[T]) Len() int { return l.size } // Iter ... -func (l *LinkedList[T]) Iter() func(func(int, T) bool) { +func (l *ObjectChain[T]) Iter() func(func(int, T) bool) { return func(yield func(int, T) bool) { i := 0 for n := l.head; n != nil; n = n.Next { diff --git a/utils/internal/linked_list_test.go b/utils/internal/object_chain_test.go similarity index 87% rename from utils/internal/linked_list_test.go rename to utils/internal/object_chain_test.go index ab57f33..168483a 100644 --- a/utils/internal/linked_list_test.go +++ b/utils/internal/object_chain_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/assert" ) -func collect[T any](l *internal.LinkedList[T]) []T { +func collect[T any](l *internal.ObjectChain[T]) []T { var out []T for _, v := range l.Iter() { out = append(out, v) @@ -15,12 +15,12 @@ func collect[T any](l *internal.LinkedList[T]) []T { return out } -func Test_LinkedList_Append(t *testing.T) { +func Test_ObjectChain_Append(t *testing.T) { t.Parallel() t.Run("ok/append-to-empty", func(t *testing.T) { t.Parallel() - var l internal.LinkedList[int] + var l internal.ObjectChain[int] // act l.Append(&internal.Node[int]{Val: 1}) @@ -36,7 +36,7 @@ func Test_LinkedList_Append(t *testing.T) { t.Run("ok/append-to-non-empty", func(t *testing.T) { t.Parallel() - var l internal.LinkedList[int] + var l internal.ObjectChain[int] n1 := &internal.Node[int]{Val: 1} n2 := &internal.Node[int]{Val: 2} @@ -54,7 +54,7 @@ func Test_LinkedList_Append(t *testing.T) { t.Run("bug/append-preserves-external-next", func(t *testing.T) { t.Parallel() - var l internal.LinkedList[int] + var l internal.ObjectChain[int] externalTail := &internal.Node[int]{Val: 9} n := &internal.Node[int]{Val: 1, Next: externalTail} @@ -68,12 +68,12 @@ func Test_LinkedList_Append(t *testing.T) { }) } -func Test_LinkedList_Remove(t *testing.T) { +func Test_ObjectChain_Remove(t *testing.T) { t.Parallel() t.Run("ok/remove-head", func(t *testing.T) { t.Parallel() - var l internal.LinkedList[int] + var l internal.ObjectChain[int] n1 := &internal.Node[int]{Val: 1} n2 := &internal.Node[int]{Val: 2} n3 := &internal.Node[int]{Val: 3} @@ -94,7 +94,7 @@ func Test_LinkedList_Remove(t *testing.T) { t.Run("ok/remove-tail", func(t *testing.T) { t.Parallel() - var l internal.LinkedList[int] + var l internal.ObjectChain[int] n1 := &internal.Node[int]{Val: 1} n2 := &internal.Node[int]{Val: 2} l.Append(n1) @@ -112,7 +112,7 @@ func Test_LinkedList_Remove(t *testing.T) { t.Run("ok/remove-middle", func(t *testing.T) { t.Parallel() - var l internal.LinkedList[int] + var l internal.ObjectChain[int] n1 := &internal.Node[int]{Val: 1} n2 := &internal.Node[int]{Val: 2} n3 := &internal.Node[int]{Val: 3} @@ -133,7 +133,7 @@ func Test_LinkedList_Remove(t *testing.T) { t.Run("ok/remove-singleton", func(t *testing.T) { t.Parallel() - var l internal.LinkedList[int] + var l internal.ObjectChain[int] n1 := &internal.Node[int]{Val: 1} l.Append(n1) @@ -147,7 +147,7 @@ func Test_LinkedList_Remove(t *testing.T) { t.Run("ok/remove-nil-noop", func(t *testing.T) { t.Parallel() - var l internal.LinkedList[int] + var l internal.ObjectChain[int] l.Append(&internal.Node[int]{Val: 1}) // act @@ -160,7 +160,7 @@ func Test_LinkedList_Remove(t *testing.T) { t.Run("bug/remove-foreign-node-decrements-size", func(t *testing.T) { t.Parallel() - var l internal.LinkedList[int] + var l internal.ObjectChain[int] l.Append(&internal.Node[int]{Val: 1}) foreign := &internal.Node[int]{Val: 999} @@ -174,7 +174,7 @@ func Test_LinkedList_Remove(t *testing.T) { t.Run("bug/remove-twice-size-negative", func(t *testing.T) { t.Parallel() - var l internal.LinkedList[int] + var l internal.ObjectChain[int] n := &internal.Node[int]{Val: 1} l.Append(n) @@ -188,12 +188,12 @@ func Test_LinkedList_Remove(t *testing.T) { }) } -func Test_LinkedList_GetHead(t *testing.T) { +func Test_ObjectChain_GetHead(t *testing.T) { t.Parallel() t.Run("edge/empty", func(t *testing.T) { t.Parallel() - var l internal.LinkedList[int] + var l internal.ObjectChain[int] // act h := l.GetHead() @@ -204,7 +204,7 @@ func Test_LinkedList_GetHead(t *testing.T) { t.Run("ok/non-empty", func(t *testing.T) { t.Parallel() - var l internal.LinkedList[int] + var l internal.ObjectChain[int] n1 := &internal.Node[int]{Val: 1} n2 := &internal.Node[int]{Val: 2} l.Append(n1) @@ -219,12 +219,12 @@ func Test_LinkedList_GetHead(t *testing.T) { }) } -func Test_LinkedList_Len(t *testing.T) { +func Test_ObjectChain_Len(t *testing.T) { t.Parallel() t.Run("ok/increments-and-decrements", func(t *testing.T) { t.Parallel() - var l internal.LinkedList[int] + var l internal.ObjectChain[int] n1 := &internal.Node[int]{Val: 1} n2 := &internal.Node[int]{Val: 2} n3 := &internal.Node[int]{Val: 3} @@ -241,12 +241,12 @@ func Test_LinkedList_Len(t *testing.T) { }) } -func Test_LinkedList_Iter(t *testing.T) { +func Test_ObjectChain_Iter(t *testing.T) { t.Parallel() t.Run("edge/empty", func(t *testing.T) { t.Parallel() - var l internal.LinkedList[int] + var l internal.ObjectChain[int] iter := l.Iter() // arrange @@ -264,7 +264,7 @@ func Test_LinkedList_Iter(t *testing.T) { t.Run("ok/full-iteration", func(t *testing.T) { t.Parallel() - var l internal.LinkedList[string] + var l internal.ObjectChain[string] l.Append(&internal.Node[string]{Val: "a"}) l.Append(&internal.Node[string]{Val: "b"}) l.Append(&internal.Node[string]{Val: "c"}) @@ -288,7 +288,7 @@ func Test_LinkedList_Iter(t *testing.T) { t.Run("ok/stop-immediately", func(t *testing.T) { t.Parallel() - var l internal.LinkedList[int] + var l internal.ObjectChain[int] l.Append(&internal.Node[int]{Val: 10}) l.Append(&internal.Node[int]{Val: 20}) l.Append(&internal.Node[int]{Val: 30}) @@ -315,7 +315,7 @@ func Test_LinkedList_Iter(t *testing.T) { t.Run("ok/stop-middle", func(t *testing.T) { t.Parallel() - var l internal.LinkedList[int] + var l internal.ObjectChain[int] l.Append(&internal.Node[int]{Val: 1}) l.Append(&internal.Node[int]{Val: 2}) l.Append(&internal.Node[int]{Val: 3}) From ed6ed61eb864d3cd6731100c04bb1db817424803 Mon Sep 17 00:00:00 2001 From: lif0 <22912194+lif0@users.noreply.github.com> Date: Thu, 30 Oct 2025 01:57:24 +0300 Subject: [PATCH 08/10] [PKG-28] feat(utils): add ObjectPool; --- utils/CHANGELOG.md | 5 +- utils/README.md | 87 +++++++++--- utils/internal/chain.go | 90 ++++++++++++ .../{object_chain_test.go => chain_test.go} | 102 +++++++------- utils/internal/object_chain.go | 94 ------------- utils/structx/object_pool.go | 118 ++++++++++++++++ utils/structx/object_pool_test.go | 43 ++++++ utils/structx/ordered_map.go | 60 +++++--- utils/structx/ordered_map_bench_test.go | 132 +++++++++++------- utils/structx/ordered_map_test.go | 53 +++++++ 10 files changed, 542 insertions(+), 242 deletions(-) create mode 100644 utils/internal/chain.go rename utils/internal/{object_chain_test.go => chain_test.go} (70%) delete mode 100644 utils/internal/object_chain.go create mode 100644 utils/structx/object_pool.go create mode 100644 utils/structx/object_pool_test.go diff --git a/utils/CHANGELOG.md b/utils/CHANGELOG.md index 21cd2d3..5b31264 100644 --- a/utils/CHANGELOG.md +++ b/utils/CHANGELOG.md @@ -11,8 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [v2.1.0] 2025-11-XX ### Added -- Set minimum go version as 1.23 -- Add struct OrderedMap[K]V; +- [PKG-18] Set minimum go version as 1.23 +- [PKG-18] Add struct OrderedMap[K]V; +- [PKG-28] Add struct ObjectPool[T]; ### Fixed ### Changed diff --git a/utils/README.md b/utils/README.md index 041478e..bad74f5 100644 --- a/utils/README.md +++ b/utils/README.md @@ -19,8 +19,8 @@ - [Examples](#-examples) - [Package: `errx`](#-package-errx) - [MultiError](#multierror) -- [Package: `typex`](#-package-typex) - - [OrderedMap](#multierror) +- [Package: `structx`](#-package-structx) + - [OrderedMap](#orderedmap) - [Roadmap](#️-roadmap) - [License](#-license) @@ -214,7 +214,7 @@ for _, job := range jobs { return me.MaybeUnwrap() ``` -## 📚 Package `typex` +## 📚 Package `structx` Provide additional golang type. @@ -229,40 +229,87 @@ The structure provide provice #### API -| Func | Complexity (time / mem) | -| -------------------------------------------------------------- | ---------------------------- | -| `(m *OrderedMap[K, V]) Get(key K) (V, bool)` | O(1) / O(1) | -| `(m *OrderedMap[K, V]) Put(key K, value V)` | O(1) / O(1) | -| `(m *OrderedMap[K, V]) Delete(key K)` | O(1) / O(1) | -| `(m *OrderedMap[K, V]) GetValues() []V` | O(N) / O(N) | -| `(m *OrderedMap[K, V]) Iter() []V` | for i,v := range m.Iter() {} | -| `Delete[K comparable, V any](m *OrderedMap[K, V], key K)` | O(1) / O(1) | +| Func | Complexity (time / mem) | +| ---------------------------------------------------------------------- | ---------------------------- | +| `(m *OrderedMap[K, V]) Get(key K) (V, bool)` | O(1) / O(1) | +| `(m *OrderedMap[K, V]) Put(key K, value V)` | O(1) / O(1) | +| `(m *OrderedMap[K, V]) GetValues() []V` | O(N) / O(N) | +| `(m *OrderedMap[K, V]) Iter() []V` | for k,v := range m.Iter() {} | +| `structx.Delete[K comparable, V any](m *OrderedMap[K, V], key K)` | O(1) / O(1) | -#### Benchmark +#### Benchmarks: OrderedMap[Type, Type1] vs map[Type]Type1 + +Environment: + +```text +goos: darwin +goarch: arm64 +cpu: Apple M2 +pkg: github.com/lif0/pkg/utils/structx +``` + +##### TL;DR + +- Inserts (`put`): `map` is faster and uses less memory. +- Lookups (`get_hit`): `OrderedMap` is faster on string keys; a bit slower on int keys. +- Deletes (`delete`): almost the same. +- Iteration (`iterate_values`): OrderedMap is much faster and ordered. + +--- + +##### Key/Value: `int, int` + +| Operation | ns/op (`OrderedMap`) | ns/op (`map`) | B/op (`OrderedMap`) | B/op (`map`) | allocs/op (`OrderedMap`) | allocs/op (`map`) | time (`OrderedMap` vs `map`) | +| -------------- | --------------: | ------------: | -------------: | -----------: | ------------------: | ----------------: | -------------------------: | +| put | 220,267 | 100,546 | 705,330 | 295,557 | 39 | 33 | **+119.1%** (2.19× slower) | +| get_hit | 74,626 | 65,668 | 0 | 0 | 0 | 0 | **+13.6%** (1.14× slower) | +| delete | 19,322 | 19,348 | 0 | 0 | 0 | 0 | **−0.1%** (≈ same) | +| iterate_values | 11,131 | 61,998 | 0 | 0 | 0 | 0 | **−82.0%** (5.6× faster) | + +##### Key/Value: `string, []string` + +| Operation | ns/op (`OrderedMap`) | ns/op (`map`) | B/op (`OrderedMap`) | B/op (`map`) | allocs/op (`OrderedMap`) | allocs/op (`map`) | Δ time (Ordered vs `map`) | +| -------------- | --------------: | ------------: | -------------: | -----------: | ------------------: | ----------------: | ------------------------: | +| put | 507,196 | 360,451 | 1,084,229 | 787,101 | 40 | 33 | **+40.7%** (1.41× slower) | +| get_hit | 136,184 | 193,829 | 0 | 0 | 0 | 0 | **−29.7%** (1.43× faster) | +| delete | 20,713 | 20,758 | 0 | 0 | 0 | 0 | **−0.2%** (≈ same) | +| iterate_values | 17,822 | 63,645 | 0 | 0 | 0 | 0 | **−72.0%** (3.6× faster) | + +##### Key/Value: `string, ComplexStruct` + +| Operation | ns/op (`OrderedMap`) | ns/op (`map`) | B/op (`OrderedMap`) | B/op (`map`) | allocs/op (`OrderedMap`) | allocs/op (`map`) | Δ time (Ordered vs `map`) | +| -------------- | --------------: | ------------: | -------------: | -----------: | ------------------: | ----------------: | ------------------------: | +| put | 493,887 | 329,433 | 1,166,137 | 918,167 | 40 | 33 | **+49.9%** (1.50× slower) | +| get_hit | 117,553 | 174,528 | 0 | 0 | 0 | 0 | **−32.6%** (1.49× faster) | +| delete | 20,471 | 20,420 | 0 | 0 | 0 | 0 | **+0.2%** (≈ same) | +| iterate_values | 19,729 | 62,435 | 0 | 0 | 0 | 0 | **−68.4%** (3.2× faster) | + +##### How to run + +```bash +go test -benchmem -run=^$ -bench ^Benchmark_OrderedMap -v github.com/lif0/pkg/utils/structx +``` -??? #### Examples ```go -import "github.com/lif0/pkg/utils/typex" +import "github.com/lif0/pkg/utils/structx" func main() { - m := typex.NewOrderedMap[string, int]() + m := structx.NewOrderedMap[string, int]() m.Put("key", 10) v, ok := m.Get("key") // v = 10 - m.Delete("key") // or build-in func typex.Delete(m, "key") + structx.Delete(m, "key") // or build-in func m.Delete("key"), but prefer build-in function. - for i,v := range m.Iter() { - fmt.Println(i,v) + for k,v := range m.Iter() { + fmt.Println(k,v) } - - fmt.Println( len( m.GetValues() ) ) // will be print '0' } ``` diff --git a/utils/internal/chain.go b/utils/internal/chain.go new file mode 100644 index 0000000..16585ef --- /dev/null +++ b/utils/internal/chain.go @@ -0,0 +1,90 @@ +package internal + +type Chain[T any] struct { + size int + head *ChainLink[T] + tail *ChainLink[T] +} + +type ChainLink[T any] struct { + Val T + Prev *ChainLink[T] + Next *ChainLink[T] +} + +// Remove ... +// time: O(1); mem: O(1) +func (c *Chain[T]) Remove(node *ChainLink[T]) { + if node == nil { + return + } + + // If a previous node exists, link it to the next one, skipping the current node. + if node.Prev != nil { + node.Prev.Next = node.Next + } + + // If a next node exists, set its previous pointer to the current node’s previous. + if node.Next != nil { + node.Next.Prev = node.Prev + } + + // if the node and the head is equal, set to head node's next. + if node == c.head { + c.head = c.head.Next + } + + // if the node and the tail is equal, set to tail node's previous. + if node == c.tail { + c.tail = c.tail.Prev + } + + c.size -= 1 +} + +// Append ... +// time: O(1); mem: O(1) +func (c *Chain[T]) Append(node *ChainLink[T]) { + if c.tail == nil { + c.head = node + c.tail = node + } else { + c.tail.Next = node + node.Prev = c.tail + c.tail = node + } + + c.size += 1 +} + +// GetHead ... +// time: O(1); mem: O(1) +func (c *Chain[T]) GetHead() *ChainLink[T] { + return c.head +} + +// Len ... +// time: O(1); mem: O(1) +func (c *Chain[T]) Len() int { + return c.size +} + +// Iter iteration on chain +// +// Example: +// +// m := Chain[int, string]() +// for i, v := range m.Iter() { +// fmt.Println(i,v) +// } +func (c *Chain[T]) Iter() func(func(int, T) bool) { + return func(yield func(int, T) bool) { + i := 0 + for n := c.head; n != nil; n = n.Next { + if !yield(i, n.Val) { + return + } + i++ + } + } +} diff --git a/utils/internal/object_chain_test.go b/utils/internal/chain_test.go similarity index 70% rename from utils/internal/object_chain_test.go rename to utils/internal/chain_test.go index 168483a..28b5acc 100644 --- a/utils/internal/object_chain_test.go +++ b/utils/internal/chain_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/assert" ) -func collect[T any](l *internal.ObjectChain[T]) []T { +func collect[T any](l *internal.Chain[T]) []T { var out []T for _, v := range l.Iter() { out = append(out, v) @@ -20,10 +20,10 @@ func Test_ObjectChain_Append(t *testing.T) { t.Run("ok/append-to-empty", func(t *testing.T) { t.Parallel() - var l internal.ObjectChain[int] + var l internal.Chain[int] // act - l.Append(&internal.Node[int]{Val: 1}) + l.Append(&internal.ChainLink[int]{Val: 1}) // assert assert.Equal(t, 1, l.Len()) @@ -36,9 +36,9 @@ func Test_ObjectChain_Append(t *testing.T) { t.Run("ok/append-to-non-empty", func(t *testing.T) { t.Parallel() - var l internal.ObjectChain[int] - n1 := &internal.Node[int]{Val: 1} - n2 := &internal.Node[int]{Val: 2} + var l internal.Chain[int] + n1 := &internal.ChainLink[int]{Val: 1} + n2 := &internal.ChainLink[int]{Val: 2} // act l.Append(n1) @@ -54,9 +54,9 @@ func Test_ObjectChain_Append(t *testing.T) { t.Run("bug/append-preserves-external-next", func(t *testing.T) { t.Parallel() - var l internal.ObjectChain[int] - externalTail := &internal.Node[int]{Val: 9} - n := &internal.Node[int]{Val: 1, Next: externalTail} + var l internal.Chain[int] + externalTail := &internal.ChainLink[int]{Val: 9} + n := &internal.ChainLink[int]{Val: 1, Next: externalTail} // act l.Append(n) @@ -73,10 +73,10 @@ func Test_ObjectChain_Remove(t *testing.T) { t.Run("ok/remove-head", func(t *testing.T) { t.Parallel() - var l internal.ObjectChain[int] - n1 := &internal.Node[int]{Val: 1} - n2 := &internal.Node[int]{Val: 2} - n3 := &internal.Node[int]{Val: 3} + var l internal.Chain[int] + n1 := &internal.ChainLink[int]{Val: 1} + n2 := &internal.ChainLink[int]{Val: 2} + n3 := &internal.ChainLink[int]{Val: 3} l.Append(n1) l.Append(n2) l.Append(n3) @@ -94,9 +94,9 @@ func Test_ObjectChain_Remove(t *testing.T) { t.Run("ok/remove-tail", func(t *testing.T) { t.Parallel() - var l internal.ObjectChain[int] - n1 := &internal.Node[int]{Val: 1} - n2 := &internal.Node[int]{Val: 2} + var l internal.Chain[int] + n1 := &internal.ChainLink[int]{Val: 1} + n2 := &internal.ChainLink[int]{Val: 2} l.Append(n1) l.Append(n2) @@ -112,10 +112,10 @@ func Test_ObjectChain_Remove(t *testing.T) { t.Run("ok/remove-middle", func(t *testing.T) { t.Parallel() - var l internal.ObjectChain[int] - n1 := &internal.Node[int]{Val: 1} - n2 := &internal.Node[int]{Val: 2} - n3 := &internal.Node[int]{Val: 3} + var l internal.Chain[int] + n1 := &internal.ChainLink[int]{Val: 1} + n2 := &internal.ChainLink[int]{Val: 2} + n3 := &internal.ChainLink[int]{Val: 3} l.Append(n1) l.Append(n2) l.Append(n3) @@ -133,8 +133,8 @@ func Test_ObjectChain_Remove(t *testing.T) { t.Run("ok/remove-singleton", func(t *testing.T) { t.Parallel() - var l internal.ObjectChain[int] - n1 := &internal.Node[int]{Val: 1} + var l internal.Chain[int] + n1 := &internal.ChainLink[int]{Val: 1} l.Append(n1) // act @@ -147,8 +147,8 @@ func Test_ObjectChain_Remove(t *testing.T) { t.Run("ok/remove-nil-noop", func(t *testing.T) { t.Parallel() - var l internal.ObjectChain[int] - l.Append(&internal.Node[int]{Val: 1}) + var l internal.Chain[int] + l.Append(&internal.ChainLink[int]{Val: 1}) // act l.Remove(nil) @@ -160,9 +160,9 @@ func Test_ObjectChain_Remove(t *testing.T) { t.Run("bug/remove-foreign-node-decrements-size", func(t *testing.T) { t.Parallel() - var l internal.ObjectChain[int] - l.Append(&internal.Node[int]{Val: 1}) - foreign := &internal.Node[int]{Val: 999} + var l internal.Chain[int] + l.Append(&internal.ChainLink[int]{Val: 1}) + foreign := &internal.ChainLink[int]{Val: 999} // act l.Remove(foreign) @@ -174,8 +174,8 @@ func Test_ObjectChain_Remove(t *testing.T) { t.Run("bug/remove-twice-size-negative", func(t *testing.T) { t.Parallel() - var l internal.ObjectChain[int] - n := &internal.Node[int]{Val: 1} + var l internal.Chain[int] + n := &internal.ChainLink[int]{Val: 1} l.Append(n) // act @@ -193,7 +193,7 @@ func Test_ObjectChain_GetHead(t *testing.T) { t.Run("edge/empty", func(t *testing.T) { t.Parallel() - var l internal.ObjectChain[int] + var l internal.Chain[int] // act h := l.GetHead() @@ -204,9 +204,9 @@ func Test_ObjectChain_GetHead(t *testing.T) { t.Run("ok/non-empty", func(t *testing.T) { t.Parallel() - var l internal.ObjectChain[int] - n1 := &internal.Node[int]{Val: 1} - n2 := &internal.Node[int]{Val: 2} + var l internal.Chain[int] + n1 := &internal.ChainLink[int]{Val: 1} + n2 := &internal.ChainLink[int]{Val: 2} l.Append(n1) l.Append(n2) @@ -224,10 +224,10 @@ func Test_ObjectChain_Len(t *testing.T) { t.Run("ok/increments-and-decrements", func(t *testing.T) { t.Parallel() - var l internal.ObjectChain[int] - n1 := &internal.Node[int]{Val: 1} - n2 := &internal.Node[int]{Val: 2} - n3 := &internal.Node[int]{Val: 3} + var l internal.Chain[int] + n1 := &internal.ChainLink[int]{Val: 1} + n2 := &internal.ChainLink[int]{Val: 2} + n3 := &internal.ChainLink[int]{Val: 3} // act l.Append(n1) @@ -246,7 +246,7 @@ func Test_ObjectChain_Iter(t *testing.T) { t.Run("edge/empty", func(t *testing.T) { t.Parallel() - var l internal.ObjectChain[int] + var l internal.Chain[int] iter := l.Iter() // arrange @@ -264,10 +264,10 @@ func Test_ObjectChain_Iter(t *testing.T) { t.Run("ok/full-iteration", func(t *testing.T) { t.Parallel() - var l internal.ObjectChain[string] - l.Append(&internal.Node[string]{Val: "a"}) - l.Append(&internal.Node[string]{Val: "b"}) - l.Append(&internal.Node[string]{Val: "c"}) + var l internal.Chain[string] + l.Append(&internal.ChainLink[string]{Val: "a"}) + l.Append(&internal.ChainLink[string]{Val: "b"}) + l.Append(&internal.ChainLink[string]{Val: "c"}) iter := l.Iter() // arrange @@ -288,10 +288,10 @@ func Test_ObjectChain_Iter(t *testing.T) { t.Run("ok/stop-immediately", func(t *testing.T) { t.Parallel() - var l internal.ObjectChain[int] - l.Append(&internal.Node[int]{Val: 10}) - l.Append(&internal.Node[int]{Val: 20}) - l.Append(&internal.Node[int]{Val: 30}) + var l internal.Chain[int] + l.Append(&internal.ChainLink[int]{Val: 10}) + l.Append(&internal.ChainLink[int]{Val: 20}) + l.Append(&internal.ChainLink[int]{Val: 30}) iter := l.Iter() // arrange @@ -315,11 +315,11 @@ func Test_ObjectChain_Iter(t *testing.T) { t.Run("ok/stop-middle", func(t *testing.T) { t.Parallel() - var l internal.ObjectChain[int] - l.Append(&internal.Node[int]{Val: 1}) - l.Append(&internal.Node[int]{Val: 2}) - l.Append(&internal.Node[int]{Val: 3}) - l.Append(&internal.Node[int]{Val: 4}) + var l internal.Chain[int] + l.Append(&internal.ChainLink[int]{Val: 1}) + l.Append(&internal.ChainLink[int]{Val: 2}) + l.Append(&internal.ChainLink[int]{Val: 3}) + l.Append(&internal.ChainLink[int]{Val: 4}) iter := l.Iter() // arrange diff --git a/utils/internal/object_chain.go b/utils/internal/object_chain.go deleted file mode 100644 index 75aa7f5..0000000 --- a/utils/internal/object_chain.go +++ /dev/null @@ -1,94 +0,0 @@ -package internal - -type ObjectChain[T any] struct { - size int - pool []*Node[T] - head *Node[T] - tail *Node[T] -} - -type Node[T any] struct { - Val T - Prev *Node[T] - Next *Node[T] -} - -func NewLinkedList[T any](cap ...int) *ObjectChain[T] { - // var llCap int - - // if len(cap) > 0 { - // llCap = cap[0] - // } - - return &ObjectChain[T]{} -} - -// Remove ... -// time: O(1); mem: O(1) -func (l *ObjectChain[T]) Remove(node *Node[T]) { - if node == nil { - return - } - - // If a previous node exists, link it to the next one, skipping the current node. - if node.Prev != nil { - node.Prev.Next = node.Next - } - - // If a next node exists, set its previous pointer to the current node’s previous. - if node.Next != nil { - node.Next.Prev = node.Prev - } - - // if the node and the head is equal, set to head node's next. - if node == l.head { - l.head = l.head.Next - } - - // if the node and the tail is equal, set to tail node's previous. - if node == l.tail { - l.tail = l.tail.Prev - } - - l.size -= 1 -} - -// Append ... -// time: O(1); mem: O(1) -func (l *ObjectChain[T]) Append(node *Node[T]) { - if l.tail == nil { - l.head = node - l.tail = node - } else { - l.tail.Next = node - node.Prev = l.tail - l.tail = node - } - - l.size += 1 -} - -// GetHead ... -// time: O(1); mem: O(1) -func (l *ObjectChain[T]) GetHead() *Node[T] { - return l.head -} - -// Len ... -// time: O(1); mem: O(1) -func (l *ObjectChain[T]) Len() int { - return l.size -} - -// Iter ... -func (l *ObjectChain[T]) Iter() func(func(int, T) bool) { - return func(yield func(int, T) bool) { - i := 0 - for n := l.head; n != nil; n = n.Next { - if !yield(i, n.Val) { - return - } - i++ - } - } -} diff --git a/utils/structx/object_pool.go b/utils/structx/object_pool.go new file mode 100644 index 0000000..acbe2ad --- /dev/null +++ b/utils/structx/object_pool.go @@ -0,0 +1,118 @@ +package structx + +type ObjectPool[T any] struct { + s uint32 + obj []*T + make func(uint32) []*T +} + +func NewObjectPool[T any](size ...uint32) *ObjectPool[T] { + var cap uint32 + if len(size) > 0 && size[0] > 0 { + cap = size[0] + } + + return &ObjectPool[T]{ + s: cap, + obj: fmakeDefault[T](cap), + make: fmakeDefault[T], + } +} + +func fmakeDefault[T any](n uint32) []*T { + objs := make([]T, n) + out := make([]*T, n) + + for i := range objs { + out[i] = &objs[i] + } + return out +} + +func (p *ObjectPool[T]) Get() *T { + if len(p.obj) == 0 { + p.allocObjects() + } + + n := len(p.obj) - 1 + x := p.obj[n] + p.obj = p.obj[:n] + return x +} + +// func (p *ObjectPool[T]) Put(x *T) { +// p.obj = append(p.obj, x) +// } + +func (p *ObjectPool[T]) allocObjects() { + const threshold = 512 + + /* Можно сделать ускорение + 00: 0 -> 1 | +inf% + 01: 1 -> 2 | +100% + 02: 2 -> 4 | +100% + 03: 4 -> 8 | +100% + 04: 8 -> 16 | +100% + 05: 16 -> 32 | +100% + 06: 32 -> 64 | +100% + 07: 64 -> 128 | +100% + 08: 128 -> 256 | +100% + 09: 256 -> 512 | +100% + 10: 512 -> 848 | +66% + 11: 848 -> 1280 | +51% + 12: 1280 -> 1792 | +40% + 13: 1792 -> 2560 | +43% + 14: 2560 -> 3408 | +33% + 15: 3408 -> 5120 | +50% + 16: 5120 -> 7168 | +40% + 17: 7168 -> 9216 | +29% + 18: 9216 -> 12288 | +33% + 19: 12288 -> 16384 | +33% + 20: 16384 -> 21504 | +31% + 21: 21504 -> 27648 | +29% + 22: 27648 -> 34816 | +26% + 23: 34816 -> 44032 | +26% + 24: 44032 -> 55296 | +26% + 25: 55296 -> 69632 | +26% + 26: 69632 -> 88064 | +26% + 27: 88064 -> 110592 | +26% + 28: 110592 -> 139264 | +26% + 29: 139264 -> 175104 | +26% + 30: 175104 -> 219136 | +25% + 31: 219136 -> 274432 | +25% + 32: 274432 -> 344064 | +25% + 33: 344064 -> 431104 | +25% + 34: 431104 -> 539648 | +25% + 35: 539648 -> 674816 | +25% + 36: 674816 -> 843776 | +25% + 37: 843776 -> 1055744 | +25% + */ + cur := p.s + + var line uint32 + if cur < threshold { + line = cur * 2 + } else { + line = cur + (cur >> 2) + } + + if line == 0 { + line = 1 + } + + if uint32(cap(p.obj)) > line { + line = uint32(cap(p.obj)) + } + + free := p.make(line) + p.obj = append(p.obj, free...) + + // diff := uint32(cap(p.obj) - len(p.obj)) + // if diff > 1 { + // free = p.make(diff) + // p.obj = append(p.obj, free...) + // line += diff + // } + + p.s = line +} diff --git a/utils/structx/object_pool_test.go b/utils/structx/object_pool_test.go new file mode 100644 index 0000000..3c36779 --- /dev/null +++ b/utils/structx/object_pool_test.go @@ -0,0 +1,43 @@ +package structx_test + +import ( + "reflect" + "testing" + "unsafe" + + "github.com/lif0/pkg/utils/structx" + "github.com/stretchr/testify/assert" +) + +type complexStruct struct { + Val int + Nums []int +} + +func Test_ObjectPool(t *testing.T) { + t.Run("ok/uniq_ptr_for_each_object", func(t *testing.T) { + pool := structx.NewObjectPool[complexStruct](1) + + hash := map[unsafe.Pointer]struct{}{} + + for i := 0; i < 10_000_000; i++ { + csp := pool.Get() + ptr := reflect.ValueOf(csp).UnsafePointer() + if _, ok := hash[ptr]; ok { + t.Fatal("sss") + } + + hash[ptr] = struct{}{} + } + + assert.Len(t, hash, 10_000_000) + }) + + t.Run("ok/", func(t *testing.T) { + pool := structx.NewObjectPool[complexStruct](0) + + x := pool.Get() + + assert.NotNil(t, x) + }) +} diff --git a/utils/structx/ordered_map.go b/utils/structx/ordered_map.go index c1cb7e1..0bcdb99 100644 --- a/utils/structx/ordered_map.go +++ b/utils/structx/ordered_map.go @@ -4,6 +4,11 @@ import ( "github.com/lif0/pkg/utils/internal" ) +type kv[K any, V any] struct { + K K + V V +} + // OrderedMap is a map[Type]Type1-like collection that preserves the order // in which keys were inserted. It behaves like a regular map but // allows deterministic iteration over its elements. @@ -11,20 +16,22 @@ import ( // OrderedMap is useful when both quick key-based access and // predictable iteration order are desired. type OrderedMap[K comparable, V any] struct { - dict map[K]*internal.Node[V] - list internal.LinkedList[V] + dict map[K]*internal.ChainLink[kv[K, V]] + list internal.Chain[kv[K, V]] + objPool *ObjectPool[internal.ChainLink[kv[K, V]]] } // NewOrderedMap returns a new empty OrderedMap. -func NewOrderedMap[K comparable, V any](cap ...int) *OrderedMap[K, V] { - var dictCap int - if len(cap) > 0 { - dictCap = cap[0] +func NewOrderedMap[K comparable, V any](size ...uint32) *OrderedMap[K, V] { + var cap uint32 = 0 + if len(size) > 0 && size[0] > 0 { + cap = size[0] } return &OrderedMap[K, V]{ - dict: make(map[K]*internal.Node[V], dictCap), - list: internal.LinkedList[V]{}, + dict: make(map[K]*internal.ChainLink[kv[K, V]], cap), + list: internal.Chain[kv[K, V]]{}, + objPool: NewObjectPool[internal.ChainLink[kv[K, V]]](cap), } } @@ -36,7 +43,7 @@ func NewOrderedMap[K comparable, V any](cap ...int) *OrderedMap[K, V] { // - mem: O(1) func (this *OrderedMap[K, V]) Get(key K) (V, bool) { if node, ok := this.dict[key]; ok { - return node.Val, true + return node.Val.V, true } var zeroVal V @@ -52,13 +59,14 @@ func (this *OrderedMap[K, V]) Get(key K) (V, bool) { // - mem: O(1) func (this *OrderedMap[K, V]) Put(key K, value V) { if node, ok := this.dict[key]; ok { - // this.removeNode(node) - // node.Val = value - // this.addNodeToTail(node) - - node.Val = value + node.Val.V = value } else { - node = &internal.Node[V]{Val: value} + node = this.objPool.Get() //&internal.Node[kv[K,V]]{Val: value} + node.Val.K = key + node.Val.V = value + node.Prev = nil // overcautiousness + node.Next = nil // overcautiousness + this.list.Append(node) this.dict[key] = node } @@ -91,26 +99,34 @@ func (this *OrderedMap[K, V]) GetValues() []V { } if cap(result) == 1 { - result[0] = this.list.GetHead().Val + result[0] = this.list.GetHead().Val.V } for i, v := range this.list.Iter() { - result[i] = v + result[i] = v.V } return result } -// Iter iteration on map +// Iter iteration on map in insertion order // // Example: // // m := NewOrderedMap[int, string]() -// for i, v := range m.Iter() { -// fmt.Println(i,v) +// +// for k, v := range m.Iter() { +// fmt.Println(k,v) // } -func (this *OrderedMap[K, V]) Iter() func(func(int, V) bool) { - return this.list.Iter() +func (this *OrderedMap[K, V]) Iter() func(func(K, V) bool) { + return func(yield func(K, V) bool) { + h := this.list.GetHead() + for n := h; n != nil; n = n.Next { + if !yield(n.Val.K, n.Val.V) { + return + } + } + } } // Delete built-in function deletes the element with the specified key diff --git a/utils/structx/ordered_map_bench_test.go b/utils/structx/ordered_map_bench_test.go index dbe9d10..4c1cda5 100644 --- a/utils/structx/ordered_map_bench_test.go +++ b/utils/structx/ordered_map_bench_test.go @@ -28,12 +28,12 @@ func makeStrs(n int) ([]string, [][]string) { return keys, vals } -func makeStrEmpties(n int) ([]string, []struct{}) { +func makeStrEmpties(n int) ([]string, []complexStruct) { keys := make([]string, n) - vals := make([]struct{}, n) + vals := make([]complexStruct, n) for i := 0; i < n; i++ { keys[i] = "k_" + itoa(i) - vals[i] = struct{}{} + vals[i] = complexStruct{Val: i, Nums: []int{n}} } return keys, vals } @@ -68,6 +68,7 @@ func Benchmark_OrderedMapIntInt(b *testing.B) { b.Run("put/orderedMap", func(b *testing.B) { b.ResetTimer() b.ReportAllocs() + for n := 0; n < b.N; n++ { m := structx.NewOrderedMap[int, int](N) for i := 0; i < N; i++ { @@ -79,6 +80,7 @@ func Benchmark_OrderedMapIntInt(b *testing.B) { b.Run("put/builtin", func(b *testing.B) { b.ResetTimer() b.ReportAllocs() + for n := 0; n < b.N; n++ { m := make(map[int]int, N) for i := 0; i < N; i++ { @@ -87,6 +89,8 @@ func Benchmark_OrderedMapIntInt(b *testing.B) { } }) + b.Log("------------------------------") + b.Run("get_hit/orderedMap", func(b *testing.B) { b.ResetTimer() b.ReportAllocs() @@ -96,6 +100,7 @@ func Benchmark_OrderedMapIntInt(b *testing.B) { } b.ResetTimer() var sink int + for n := 0; n < b.N; n++ { for i := 0; i < N; i++ { v, _ := m.Get(keys[i]) @@ -116,40 +121,46 @@ func Benchmark_OrderedMapIntInt(b *testing.B) { var sink int for n := 0; n < b.N; n++ { for i := 0; i < N; i++ { - sink ^= m[keys[i]] + v, _ := m[keys[i]] + sink ^= v } } _ = sink }) + b.Log("------------------------------") + b.Run("delete/orderedMap", func(b *testing.B) { - b.ResetTimer() b.ReportAllocs() + m := structx.NewOrderedMap[int, int](N) + for i := 0; i < N; i++ { + m.Put(keys[i], vals[i]) + } + b.ResetTimer() for n := 0; n < b.N; n++ { - m := structx.NewOrderedMap[int, int](N) for i := 0; i < N; i++ { - m.Put(keys[i], vals[i]) - } - for i := 0; i < N; i++ { - m.Delete(keys[i]) + structx.Delete(m, keys[i]) } } }) b.Run("delete/builtin", func(b *testing.B) { - b.ResetTimer() b.ReportAllocs() + m := make(map[int]int, N) + for i := 0; i < N; i++ { + m[keys[i]] = vals[i] + } + + b.ResetTimer() for n := 0; n < b.N; n++ { - m := make(map[int]int, N) - for i := 0; i < N; i++ { - m[keys[i]] = vals[i] - } for i := 0; i < N; i++ { delete(m, keys[i]) } } }) + b.Log("------------------------------") + b.Run("iterate_values/orderedMap", func(b *testing.B) { b.ResetTimer() b.ReportAllocs() @@ -160,7 +171,7 @@ func Benchmark_OrderedMapIntInt(b *testing.B) { b.ResetTimer() var sink int for n := 0; n < b.N; n++ { - for v := range m.GetValues() { + for _, v := range m.Iter() { sink ^= v } } @@ -190,9 +201,10 @@ func Benchmark_OrderedMap_vs_Builtin_StringSlice(b *testing.B) { keys, vals := makeStrs(N) b.Run("put/ordered", func(b *testing.B) { + b.ResetTimer() b.ReportAllocs() for n := 0; n < b.N; n++ { - m := structx.NewOrderedMap[string, []string]() + m := structx.NewOrderedMap[string, []string](N) for i := 0; i < N; i++ { m.Put(keys[i], vals[i]) } @@ -200,6 +212,7 @@ func Benchmark_OrderedMap_vs_Builtin_StringSlice(b *testing.B) { }) b.Run("put/builtin", func(b *testing.B) { + b.ResetTimer() b.ReportAllocs() for n := 0; n < b.N; n++ { m := make(map[string][]string, N) @@ -209,9 +222,11 @@ func Benchmark_OrderedMap_vs_Builtin_StringSlice(b *testing.B) { } }) + b.Log("------------------------------") + b.Run("get_hit/ordered", func(b *testing.B) { b.ReportAllocs() - m := structx.NewOrderedMap[string, []string]() + m := structx.NewOrderedMap[string, []string](N) for i := 0; i < N; i++ { m.Put(keys[i], vals[i]) } @@ -220,7 +235,6 @@ func Benchmark_OrderedMap_vs_Builtin_StringSlice(b *testing.B) { for n := 0; n < b.N; n++ { for i := 0; i < N; i++ { v, _ := m.Get(keys[i]) - // потребляем длину, чтобы компилятор не выкинул if len(v) > 0 { sink ^= len(v[0]) } @@ -239,7 +253,7 @@ func Benchmark_OrderedMap_vs_Builtin_StringSlice(b *testing.B) { var sink int for n := 0; n < b.N; n++ { for i := 0; i < N; i++ { - v := m[keys[i]] + v, _ := m[keys[i]] if len(v) > 0 { sink ^= len(v[0]) } @@ -248,46 +262,49 @@ func Benchmark_OrderedMap_vs_Builtin_StringSlice(b *testing.B) { _ = sink }) + b.Log("------------------------------") + b.Run("delete/ordered", func(b *testing.B) { b.ReportAllocs() + m := structx.NewOrderedMap[string, []string](N) + for i := 0; i < N; i++ { + m.Put(keys[i], vals[i]) + } + b.ResetTimer() for n := 0; n < b.N; n++ { - m := structx.NewOrderedMap[string, []string]() for i := 0; i < N; i++ { - m.Put(keys[i], vals[i]) - } - for i := 0; i < N; i++ { - m.Delete(keys[i]) + structx.Delete(m, keys[i]) } } }) b.Run("delete/builtin", func(b *testing.B) { b.ReportAllocs() + m := make(map[string][]string, N) + for i := 0; i < N; i++ { + m[keys[i]] = vals[i] + } + b.ResetTimer() for n := 0; n < b.N; n++ { - m := make(map[string][]string, N) - for i := 0; i < N; i++ { - m[keys[i]] = vals[i] - } for i := 0; i < N; i++ { delete(m, keys[i]) } } }) + b.Log("------------------------------") + b.Run("iterate_values/ordered", func(b *testing.B) { b.ReportAllocs() - m := structx.NewOrderedMap[string, []string]() + m := structx.NewOrderedMap[string, []string](N) for i := 0; i < N; i++ { m.Put(keys[i], vals[i]) } b.ResetTimer() var sink int for n := 0; n < b.N; n++ { - vs := m.GetValues() - for i := 0; i < len(vs); i++ { - if len(vs[i]) > 1 { - sink ^= len(vs[i][1]) - } + for range m.Iter() { + sink ^= 1 } } _ = sink @@ -312,14 +329,15 @@ func Benchmark_OrderedMap_vs_Builtin_StringSlice(b *testing.B) { }) } -func Benchmark_OrderedMap_vs_Builtin_StringEmptyStruct(b *testing.B) { +func Benchmark_OrderedMap_vs_Builtin_StringComplexStruct(b *testing.B) { const N = 1_0000 keys, vals := makeStrEmpties(N) b.Run("put/ordered", func(b *testing.B) { + b.ResetTimer() b.ReportAllocs() for n := 0; n < b.N; n++ { - m := structx.NewOrderedMap[string, struct{}]() + m := structx.NewOrderedMap[string, complexStruct](N) for i := 0; i < N; i++ { m.Put(keys[i], vals[i]) } @@ -327,18 +345,21 @@ func Benchmark_OrderedMap_vs_Builtin_StringEmptyStruct(b *testing.B) { }) b.Run("put/builtin", func(b *testing.B) { + b.ResetTimer() b.ReportAllocs() for n := 0; n < b.N; n++ { - m := make(map[string]struct{}, N) + m := make(map[string]complexStruct, N) for i := 0; i < N; i++ { m[keys[i]] = vals[i] } } }) + b.Log("------------------------------") + b.Run("get_hit/ordered", func(b *testing.B) { b.ReportAllocs() - m := structx.NewOrderedMap[string, struct{}]() + m := structx.NewOrderedMap[string, complexStruct](N) for i := 0; i < N; i++ { m.Put(keys[i], vals[i]) } @@ -357,7 +378,7 @@ func Benchmark_OrderedMap_vs_Builtin_StringEmptyStruct(b *testing.B) { b.Run("get_hit/builtin", func(b *testing.B) { b.ReportAllocs() - m := make(map[string]struct{}, N) + m := make(map[string]complexStruct, N) for i := 0; i < N; i++ { m[keys[i]] = vals[i] } @@ -374,43 +395,48 @@ func Benchmark_OrderedMap_vs_Builtin_StringEmptyStruct(b *testing.B) { _ = sink }) + b.Log("------------------------------") + b.Run("delete/ordered", func(b *testing.B) { b.ReportAllocs() + m := structx.NewOrderedMap[string, complexStruct](N) + for i := 0; i < N; i++ { + m.Put(keys[i], vals[i]) + } + b.ResetTimer() for n := 0; n < b.N; n++ { - m := structx.NewOrderedMap[string, struct{}]() - for i := 0; i < N; i++ { - m.Put(keys[i], vals[i]) - } for i := 0; i < N; i++ { - m.Delete(keys[i]) + structx.Delete(m, keys[i]) } } }) b.Run("delete/builtin", func(b *testing.B) { b.ReportAllocs() + m := make(map[string]complexStruct, N) + for i := 0; i < N; i++ { + m[keys[i]] = vals[i] + } + b.ResetTimer() for n := 0; n < b.N; n++ { - m := make(map[string]struct{}, N) - for i := 0; i < N; i++ { - m[keys[i]] = vals[i] - } for i := 0; i < N; i++ { delete(m, keys[i]) } } }) + b.Log("------------------------------") + b.Run("iterate_values/ordered", func(b *testing.B) { b.ReportAllocs() - m := structx.NewOrderedMap[string, struct{}]() + m := structx.NewOrderedMap[string, complexStruct](N) for i := 0; i < N; i++ { m.Put(keys[i], vals[i]) } b.ResetTimer() var sink int for n := 0; n < b.N; n++ { - vs := m.GetValues() - for i := 0; i < len(vs); i++ { + for range m.Iter() { sink ^= 1 } } @@ -419,7 +445,7 @@ func Benchmark_OrderedMap_vs_Builtin_StringEmptyStruct(b *testing.B) { b.Run("iterate_values/builtin_range", func(b *testing.B) { b.ReportAllocs() - m := make(map[string]struct{}, N) + m := make(map[string]complexStruct, N) for i := 0; i < N; i++ { m[keys[i]] = vals[i] } diff --git a/utils/structx/ordered_map_test.go b/utils/structx/ordered_map_test.go index 86a7111..267e5e1 100644 --- a/utils/structx/ordered_map_test.go +++ b/utils/structx/ordered_map_test.go @@ -8,6 +8,59 @@ import ( "github.com/lif0/pkg/utils/structx" ) +func Test_OrderedMap_Iter(t *testing.T) { + t.Parallel() + + t.Run("ok/iter-kv", func(t *testing.T) { + t.Parallel() + m := structx.NewOrderedMap[int, int](10) + res := 0 + + // act + for i := range 10 { + m.Put(i+1, i+1) + } + for k, v := range m.Iter() { + res += k + v + } + + // assert + assert.Equal(t, 110, res) + }) + + t.Run("ok/iter-kv-2", func(t *testing.T) { + t.Parallel() + m := structx.NewOrderedMap[int, int]() + + // act + for i := range 10 { + m.Put(i, i) + } + + // assert + expected := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} + gotKey := make([]int, 0, 10) + gotValue := make([]int, 0, 10) + for k, v := range m.Iter() { + gotKey = append(gotKey, k) + gotValue = append(gotValue, v) + } + assert.Equal(t, expected, gotKey) + assert.Equal(t, expected, gotValue) + }) + + t.Run("ok/iter-kv-3", func(t *testing.T) { + t.Parallel() + m := structx.NewOrderedMap[int, int]() + + m.Put(1, 1) + + m.Iter()(func(i1, i2 int) bool { + return false + }) + }) +} + func Test_OrderedMap_Get(t *testing.T) { t.Parallel() From bda78d4350716aa6b32875ff8b6c7da8b7b8c11e Mon Sep 17 00:00:00 2001 From: lif0 <22912194+lif0@users.noreply.github.com> Date: Thu, 30 Oct 2025 23:59:53 +0300 Subject: [PATCH 09/10] [PKG-28] docs(utils): update README.md; --- utils/README.md | 6 +++--- utils/structx/ordered_map.go | 5 +---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/utils/README.md b/utils/README.md index bad74f5..ec3106e 100644 --- a/utils/README.md +++ b/utils/README.md @@ -260,7 +260,7 @@ pkg: github.com/lif0/pkg/utils/structx ##### Key/Value: `int, int` -| Operation | ns/op (`OrderedMap`) | ns/op (`map`) | B/op (`OrderedMap`) | B/op (`map`) | allocs/op (`OrderedMap`) | allocs/op (`map`) | time (`OrderedMap` vs `map`) | +| Operation | ns/op (`OrderedMap`) | ns/op (`map`) | B/op (`OrderedMap`) | B/op (`map`) | allocs/op (`OrderedMap`) | allocs/op (`map`) | time (`OrderedMap` vs `map`) | | -------------- | --------------: | ------------: | -------------: | -----------: | ------------------: | ----------------: | -------------------------: | | put | 220,267 | 100,546 | 705,330 | 295,557 | 39 | 33 | **+119.1%** (2.19× slower) | | get_hit | 74,626 | 65,668 | 0 | 0 | 0 | 0 | **+13.6%** (1.14× slower) | @@ -269,7 +269,7 @@ pkg: github.com/lif0/pkg/utils/structx ##### Key/Value: `string, []string` -| Operation | ns/op (`OrderedMap`) | ns/op (`map`) | B/op (`OrderedMap`) | B/op (`map`) | allocs/op (`OrderedMap`) | allocs/op (`map`) | Δ time (Ordered vs `map`) | +| Operation | ns/op (`OrderedMap`) | ns/op (`map`) | B/op (`OrderedMap`) | B/op (`map`) | allocs/op (`OrderedMap`) | allocs/op (`map`) | time (Ordered vs `map`) | | -------------- | --------------: | ------------: | -------------: | -----------: | ------------------: | ----------------: | ------------------------: | | put | 507,196 | 360,451 | 1,084,229 | 787,101 | 40 | 33 | **+40.7%** (1.41× slower) | | get_hit | 136,184 | 193,829 | 0 | 0 | 0 | 0 | **−29.7%** (1.43× faster) | @@ -278,7 +278,7 @@ pkg: github.com/lif0/pkg/utils/structx ##### Key/Value: `string, ComplexStruct` -| Operation | ns/op (`OrderedMap`) | ns/op (`map`) | B/op (`OrderedMap`) | B/op (`map`) | allocs/op (`OrderedMap`) | allocs/op (`map`) | Δ time (Ordered vs `map`) | +| Operation | ns/op (`OrderedMap`) | ns/op (`map`) | B/op (`OrderedMap`) | B/op (`map`) | allocs/op (`OrderedMap`) | allocs/op (`map`) | time (Ordered vs `map`) | | -------------- | --------------: | ------------: | -------------: | -----------: | ------------------: | ----------------: | ------------------------: | | put | 493,887 | 329,433 | 1,166,137 | 918,167 | 40 | 33 | **+49.9%** (1.50× slower) | | get_hit | 117,553 | 174,528 | 0 | 0 | 0 | 0 | **−32.6%** (1.49× faster) | diff --git a/utils/structx/ordered_map.go b/utils/structx/ordered_map.go index 0bcdb99..1e02f1a 100644 --- a/utils/structx/ordered_map.go +++ b/utils/structx/ordered_map.go @@ -79,10 +79,7 @@ func (this *OrderedMap[K, V]) Put(key K, value V) { // - time: O(1) // - mem: O(1) func (this *OrderedMap[K, V]) Delete(key K) { - if node, ok := this.dict[key]; ok { - this.list.Remove(node) - delete(this.dict, key) - } + Delete(this, key) } // GetValues returns all values in insertion order. From 2915502dde6cf4283601addb172a2a8910c06fc2 Mon Sep 17 00:00:00 2001 From: lif0 <22912194+lif0@users.noreply.github.com> Date: Fri, 31 Oct 2025 00:01:37 +0300 Subject: [PATCH 10/10] [PKG-28] docs(utils): update README.md; --- utils/README.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/utils/README.md b/utils/README.md index ec3106e..d7d4145 100644 --- a/utils/README.md +++ b/utils/README.md @@ -315,14 +315,12 @@ func main() { ## 🗺️ Roadmap -The future direction of this package is community-driven! Ideas and contributions are highly welcome. +- [ ] improve Object Order +- [ ] improve perf for OrderedMap -☹️ No idea - -Contributions and ideas are welcome! 🤗 +--- -**Contributions:** -Feel free to open an Issue to discuss a new idea or a Pull Request to implement it! 🤗 +Contributions and feature suggestions are welcome 🤗. ---