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/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/CHANGELOG.md b/utils/CHANGELOG.md index a95758c..5b31264 100644 --- a/utils/CHANGELOG.md +++ b/utils/CHANGELOG.md @@ -9,6 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed ### Changed +## [v2.1.0] 2025-11-XX +### Added +- [PKG-18] Set minimum go version as 1.23 +- [PKG-18] Add struct OrderedMap[K]V; +- [PKG-28] Add struct ObjectPool[T]; +### 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..d7d4145 100644 --- a/utils/README.md +++ b/utils/README.md @@ -19,6 +19,8 @@ - [Examples](#-examples) - [Package: `errx`](#-package-errx) - [MultiError](#multierror) +- [Package: `structx`](#-package-structx) + - [OrderedMap](#orderedmap) - [Roadmap](#️-roadmap) - [License](#-license) @@ -34,7 +36,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 @@ -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,16 +214,113 @@ for _, job := range jobs { return me.MaybeUnwrap() ``` -## 🗺️ Roadmap +## 📚 Package `structx` + +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]) 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) | + + +#### 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) | -The future direction of this package is community-driven! Ideas and contributions are highly welcome. +##### Key/Value: `string, []string` -☹️ No idea +| 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) | -Contributions and ideas are welcome! 🤗 +##### 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/structx" + + +func main() { + m := structx.NewOrderedMap[string, int]() + + m.Put("key", 10) + + v, ok := m.Get("key") // v = 10 + + structx.Delete(m, "key") // or build-in func m.Delete("key"), but prefer build-in function. + + for k,v := range m.Iter() { + fmt.Println(k,v) + } +} +``` + +## 🗺️ Roadmap + +- [ ] improve Object Order +- [ ] improve perf for OrderedMap + +--- -**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 🤗. --- 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/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/chain_test.go b/utils/internal/chain_test.go new file mode 100644 index 0000000..28b5acc --- /dev/null +++ b/utils/internal/chain_test.go @@ -0,0 +1,343 @@ +package internal_test + +import ( + "testing" + + "github.com/lif0/pkg/utils/internal" + "github.com/stretchr/testify/assert" +) + +func collect[T any](l *internal.Chain[T]) []T { + var out []T + for _, v := range l.Iter() { + out = append(out, v) + } + return out +} + +func Test_ObjectChain_Append(t *testing.T) { + t.Parallel() + + t.Run("ok/append-to-empty", func(t *testing.T) { + t.Parallel() + var l internal.Chain[int] + + // act + l.Append(&internal.ChainLink[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.Chain[int] + n1 := &internal.ChainLink[int]{Val: 1} + n2 := &internal.ChainLink[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.Chain[int] + externalTail := &internal.ChainLink[int]{Val: 9} + n := &internal.ChainLink[int]{Val: 1, Next: externalTail} + + // act + l.Append(n) + + // assert + assert.Equal(t, 1, l.Len()) + got := collect(&l) + assert.Equal(t, []int{1, 9}, got) + }) +} + +func Test_ObjectChain_Remove(t *testing.T) { + t.Parallel() + + t.Run("ok/remove-head", func(t *testing.T) { + t.Parallel() + 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) + + // 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.Chain[int] + n1 := &internal.ChainLink[int]{Val: 1} + n2 := &internal.ChainLink[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.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) + + // 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.Chain[int] + n1 := &internal.ChainLink[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.Chain[int] + l.Append(&internal.ChainLink[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.Chain[int] + l.Append(&internal.ChainLink[int]{Val: 1}) + foreign := &internal.ChainLink[int]{Val: 999} + + // act + l.Remove(foreign) + + // assert + 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.Chain[int] + n := &internal.ChainLink[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_ObjectChain_GetHead(t *testing.T) { + t.Parallel() + + t.Run("edge/empty", func(t *testing.T) { + t.Parallel() + var l internal.Chain[int] + + // act + h := l.GetHead() + + // assert + assert.Nil(t, h) + }) + + t.Run("ok/non-empty", func(t *testing.T) { + t.Parallel() + var l internal.Chain[int] + n1 := &internal.ChainLink[int]{Val: 1} + n2 := &internal.ChainLink[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_ObjectChain_Len(t *testing.T) { + t.Parallel() + + t.Run("ok/increments-and-decrements", func(t *testing.T) { + t.Parallel() + 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) + 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_ObjectChain_Iter(t *testing.T) { + t.Parallel() + + t.Run("edge/empty", func(t *testing.T) { + t.Parallel() + var l internal.Chain[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.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 + 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.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 + 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.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 + 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/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 new file mode 100644 index 0000000..1e02f1a --- /dev/null +++ b/utils/structx/ordered_map.go @@ -0,0 +1,151 @@ +package structx + +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. +// +// 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.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](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.ChainLink[kv[K, V]], cap), + list: internal.Chain[kv[K, V]]{}, + objPool: NewObjectPool[internal.ChainLink[kv[K, V]]](cap), + } +} + +// 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.V, true + } + + var zeroVal V + return zeroVal, false +} + +// 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 { + node.Val.V = value + } else { + 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 + } +} + +// 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) { + Delete(this, key) +} + +// 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()) + + if cap(result) == 0 { + return result + } + + if cap(result) == 1 { + result[0] = this.list.GetHead().Val.V + } + + for i, v := range this.list.Iter() { + result[i] = v.V + } + + return result +} + +// Iter iteration on map in insertion order +// +// Example: +// +// m := NewOrderedMap[int, string]() +// +// for k, v := range m.Iter() { +// fmt.Println(k,v) +// } +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 +// (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) +// structx.Delete(om, "x") +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) + } +} diff --git a/utils/structx/ordered_map_bench_test.go b/utils/structx/ordered_map_bench_test.go new file mode 100644 index 0000000..4c1cda5 --- /dev/null +++ b/utils/structx/ordered_map_bench_test.go @@ -0,0 +1,461 @@ +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, []complexStruct) { + keys := make([]string, n) + vals := make([]complexStruct, n) + for i := 0; i < n; i++ { + keys[i] = "k_" + itoa(i) + vals[i] = complexStruct{Val: i, Nums: []int{n}} + } + 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.Log("------------------------------") + + 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++ { + v, _ := m[keys[i]] + sink ^= v + } + } + _ = sink + }) + + b.Log("------------------------------") + + b.Run("delete/orderedMap", func(b *testing.B) { + 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++ { + for i := 0; i < N; i++ { + structx.Delete(m, keys[i]) + } + } + }) + + b.Run("delete/builtin", func(b *testing.B) { + 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++ { + 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() + 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.Iter() { + 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.ResetTimer() + b.ReportAllocs() + for n := 0; n < b.N; n++ { + m := structx.NewOrderedMap[string, []string](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[string][]string, 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, []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++ { + 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.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++ { + for i := 0; i < N; 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++ { + 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](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 range m.Iter() { + sink ^= 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_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, complexStruct](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[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, 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++ { + 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]complexStruct, 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.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++ { + for i := 0; i < N; 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++ { + 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, 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++ { + for range m.Iter() { + sink ^= 1 + } + } + _ = sink + }) + + b.Run("iterate_values/builtin_range", 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() + var sink int + for n := 0; n < b.N; n++ { + for range m { + sink ^= 1 + } + } + _ = sink + }) +} diff --git a/utils/structx/ordered_map_test.go b/utils/structx/ordered_map_test.go new file mode 100644 index 0000000..267e5e1 --- /dev/null +++ b/utils/structx/ordered_map_test.go @@ -0,0 +1,323 @@ +package structx_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "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() + + t.Run("ok/after-put", func(t *testing.T) { + t.Parallel() + m := structx.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 := structx.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 structx.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 structx.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 := structx.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 := structx.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 := structx.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 := structx.NewOrderedMap[string, int]() + m.Put("a", 1) + m.Put("c", 3) + + // act + structx.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 structx.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 := structx.NewOrderedMap[string, int]() + m.Put("a", 1) + m.Put("b", 2) + m.Put("c", 3) + + // act + structx.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 := structx.NewOrderedMap[string, int]() + m.Put("a", 1) + m.Put("c", 3) + + // act + structx.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 *structx.OrderedMap[int, int] // nil + + // act: should't panic + structx.Delete(m, 1) + + // assert + assert.Nil(t, m) + }) + + t.Run("ok/empty", func(t *testing.T) { + t.Parallel() + m := structx.NewOrderedMap[int, int]() + + // act: should't panic + structx.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 structx.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 := structx.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 := structx.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 := structx.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)) + }) +}