How to create recursive concurrent tree search in go - go

Is there a proper way to implement concurrency inside recursive tree search in go?
The following code shows basic recursive tree search for a tree of nodes having id and children properties.
If I find a result I can close the channel but in case there is no result how to deal with that?
// Node is a struct containing int field ID and []*Node field Children.
type Node struct {
ID int
Children []*Node
}
// Find search for a record by id inside an array of records
func Find(node *Node, id int) *Node {
if node.ID == id {
return node
}
for _, c := range node.Children {
found := Find(c, id)
if found != nil {
return found
}
}
return nil
}

here is a simple and stupid benchmark that i hope will be helpful to OP to better understand impacts of simplistic implementations of concurrent programming.
package main_test
import (
"sync"
"testing"
)
func SearchNonConcurrently(v int, mySlice []int) int {
for idx, i := range mySlice {
if i == v {
return idx
}
}
return -1
}
func SearchConcurrently(v int, mySlice []int) int {
var wg sync.WaitGroup
wg.Add(len(mySlice))
res := make(chan int)
for idx, i := range mySlice {
go func(idx, i int) {
defer wg.Done()
if i == v {
res <- idx
}
}(idx, i)
}
go func() {
wg.Wait()
close(res)
}()
idx, ok := <-res
if !ok {
return -1
}
return idx
}
func BenchmarkSearchNonConcurrently(b *testing.B) {
mySlice := make([]int, 10000)
mySlice[len(mySlice)-1] = 1
for n := 0; n < b.N; n++ {
SearchNonConcurrently(1, mySlice)
}
}
func BenchmarkSearchConcurrently(b *testing.B) {
mySlice := make([]int, 10000)
mySlice[len(mySlice)-1] = 1
for n := 0; n < b.N; n++ {
SearchConcurrently(1, mySlice)
}
}
The results:
$ go test -bench=. -benchmem -count=4
goos: linux
goarch: amd64
pkg: test/bencslice
BenchmarkSearchNonConcurrently-4 85780 13831 ns/op 0 B/op 0 allocs/op
BenchmarkSearchNonConcurrently-4 83619 13880 ns/op 0 B/op 0 allocs/op
BenchmarkSearchNonConcurrently-4 87128 14050 ns/op 0 B/op 0 allocs/op
BenchmarkSearchNonConcurrently-4 87366 13837 ns/op 0 B/op 0 allocs/op
BenchmarkSearchConcurrently-4 235 4867257 ns/op 2200 B/op 6 allocs/op
BenchmarkSearchConcurrently-4 244 4896937 ns/op 3360 B/op 8 allocs/op
BenchmarkSearchConcurrently-4 240 4871934 ns/op 453 B/op 2 allocs/op
BenchmarkSearchConcurrently-4 244 4884617 ns/op 3352 B/op 8 allocs/op
PASS
ok test/bencslice 12.087s
$ go version
go version go1.15.2 linux/amd64

I created the follwing code. My only regret is that it keeps creating go routines even after finding the solution. I mean for a sorted tree with 1000 elements if I search for node with ID = 5 it keeps opening cases with above 900.
func FindConcurrent(node *Node, id int, c chan *Node) {
select {
case <-c:
return
default:
}
if node.ID == id {
c <- node
close(c)
return
}
for _, child := range node.Children {
go FindConcurrent(child, id, c)
}
}

Related

go maps non-performant for large number of keys

I discovered very strange behaviour with go maps recently. The use case is to create a group of integers and have O(1) check for IsMember(id int).
The current implementation is :
func convertToMap(v []int64) map[int64]void {
out := make(map[int64]void, len(v))
for _, i := range v {
out[i] = void{}
}
return out
}
type Group struct {
members map[int64]void
}
type void struct{}
func (g *Group) IsMember(input string) (ok bool) {
memberID, _ := strconv.ParseInt(input, 10, 64)
_, ok = g.members[memberID]
return
}
When i benchmark the IsMember method, until 6 million members, everything looks fine. But above that the map look up is taking 1 second for each lookup!!
The benchmark test:
func BenchmarkIsMember(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
g := &Group{}
g.members = convertToMap(benchmarkV)
for N := 0; N < b.N && N < sizeOfGroup; N++ {
g.IsMember(benchmarkKVString[N])
}
}
var benchmarkV, benchmarkKVString = func(size int) ([]int64, []string{
v := make([]int64, size)
s := make([]string, size)
for i := range v {
val := rand.Int63()
v[i] = val
s[i] = strconv.FormatInt(val, 10)
}
return v, s
}(sizeOfGroup)
Benchmark numbers:
const sizeOfGroup = 6000000
BenchmarkIsMember-8 2000000 568 ns/op 50 B/op 0 allocs/op
const sizeOfGroup = 6830000
BenchmarkIsMember-8 1 1051725455 ns/op 178767208 B/op 25 allocs/op
Anything above group size of 6.8 million gives the same result.
Can someone help me to explain why this is happening, and can anything be done to make this performant while still using maps?
Also, i dont understand why so much memory is being allocated? Even if the time taken is due to collision and then linked list traversal, there shouldn't be any mem allocation, is my thought process wrong?
No need to measure extra allocation for converting slice to map because we just want to measure the lookup operation.
I've slightly modify the benchmark:
func BenchmarkIsMember(b *testing.B) {
fn := func(size int) ([]int64, []string) {
v := make([]int64, size)
s := make([]string, size)
for i := range v {
val := rand.Int63()
v[i] = val
s[i] = strconv.FormatInt(val, 10)
}
return v, s
}
for _, size := range []int{
6000000,
6800000,
6830000,
60000000,
} {
b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) {
var benchmarkV, benchmarkKVString = fn(size)
g := &deltaGroup{}
g.members = convertToMap(benchmarkV)
b.ReportAllocs()
b.ResetTimer()
for N := 0; N < b.N && N < size; N++ {
g.IsMember(benchmarkKVString[N])
}
})
}
}
And got the following results:
go test ./... -bench=. -benchtime=10s -cpu=1
goos: linux
goarch: amd64
pkg: trash
BenchmarkIsMember/size=6000000 2000000000 0.55 ns/op 0 B/op 0 allocs/op
BenchmarkIsMember/size=6800000 1000000000 1.27 ns/op 0 B/op 0 allocs/op
BenchmarkIsMember/size=6830000 1000000000 1.23 ns/op 0 B/op 0 allocs/op
BenchmarkIsMember/size=60000000 100000000 136 ns/op 0 B/op 0 allocs/op
PASS
ok trash 167.578s
Degradation isn't so significant as in your example.

Benchmarking bad results

so I have the Quicksort algorithm implemented with concurrency (the one without as well). Now I wanted to compare the times. I wrote this:
func benchmarkConcurrentQuickSort(size int, b *testing.B) {
A := RandomArray(size)
var wg sync.WaitGroup
b.ResetTimer()
ConcurrentQuicksort(A, 0, len(A)-1, &wg)
wg.Wait()
}
func BenchmarkConcurrentQuickSort500(b *testing.B) {
benchmarkConcurrentQuickSort(500, b)
}
func BenchmarkConcurrentQuickSort1000(b *testing.B) {
benchmarkConcurrentQuickSort(1000, b)
}
func BenchmarkConcurrentQuickSort5000(b *testing.B) {
benchmarkConcurrentQuickSort(5000, b)
}
func BenchmarkConcurrentQuickSort10000(b *testing.B) {
benchmarkConcurrentQuickSort(10000, b)
}
func BenchmarkConcurrentQuickSort20000(b *testing.B) {
benchmarkConcurrentQuickSort(20000, b)
}
func BenchmarkConcurrentQuickSort1000000(b *testing.B) {
benchmarkConcurrentQuickSort(1000000, b)
}
The results are like this:
C:\projects\go\src\github.com\frynio\mysort>go test -bench=.
BenchmarkConcurrentQuickSort500-4 2000000000 0.00 ns/op
BenchmarkConcurrentQuickSort1000-4 2000000000 0.00 ns/op
BenchmarkConcurrentQuickSort5000-4 2000000000 0.00 ns/op
BenchmarkConcurrentQuickSort10000-4 2000000000 0.00 ns/op
BenchmarkConcurrentQuickSort20000-4 2000000000 0.00 ns/op
BenchmarkConcurrentQuickSort1000000-4 30 49635266 ns/op
PASS
ok github.com/frynio/mysort 8.342s
I can believe the last one, but I definitely think that sorting 500-element array takes longer than 1ns. What am i doing wrong? I am pretty sure that RandomArray returns array of wanted size, as we can see in the last benchmark. Why does it print out the 0.00 ns?
func RandomArray(n int) []int {
a := []int{}
for i := 0; i < n; i++ {
a = append(a, rand.Intn(500))
}
return a
}
// ConcurrentPartition - ConcurrentQuicksort function for partitioning the array (randomized choice of a pivot)
func ConcurrentPartition(A []int, p int, r int) int {
index := rand.Intn(r-p) + p
pivot := A[index]
A[index] = A[r]
A[r] = pivot
x := A[r]
j := p - 1
i := p
for i < r {
if A[i] <= x {
j++
tmp := A[j]
A[j] = A[i]
A[i] = tmp
}
i++
}
temp := A[j+1]
A[j+1] = A[r]
A[r] = temp
return j + 1
}
// ConcurrentQuicksort - a concurrent version of a quicksort algorithm
func ConcurrentQuicksort(A []int, p int, r int, wg *sync.WaitGroup) {
if p < r {
q := ConcurrentPartition(A, p, r)
wg.Add(2)
go func() {
ConcurrentQuicksort(A, p, q-1, wg)
wg.Done()
}()
go func() {
ConcurrentQuicksort(A, q+1, r, wg)
wg.Done()
}()
}
}
Package testing
A sample benchmark function looks like this:
func BenchmarkHello(b *testing.B) {
for i := 0; i < b.N; i++ {
fmt.Sprintf("hello")
}
}
The benchmark function must run the target code b.N times. During
benchmark execution, b.N is adjusted until the benchmark function
lasts long enough to be timed reliably.
I don't see a benchmark loop in your code. Try
func benchmarkConcurrentQuickSort(size int, b *testing.B) {
A := RandomArray(size)
var wg sync.WaitGroup
b.ResetTimer()
for i := 0; i < b.N; i++ {
ConcurrentQuicksort(A, 0, len(A)-1, &wg)
wg.Wait()
}
}
Output:
BenchmarkConcurrentQuickSort500-4 10000 122291 ns/op
BenchmarkConcurrentQuickSort1000-4 5000 221154 ns/op
BenchmarkConcurrentQuickSort5000-4 1000 1225230 ns/op
BenchmarkConcurrentQuickSort10000-4 500 2568024 ns/op
BenchmarkConcurrentQuickSort20000-4 300 5808130 ns/op
BenchmarkConcurrentQuickSort1000000-4 1 1371751710 ns/op

Performance of function slice parameter vs global variable?

I've got the following function:
func checkFiles(path string, excludedPatterns []string) {
// ...
}
I'm wondering, since excludedPatterns never changes, should I optimize it by making the var global (and not passing it to the function every time), or does Golang already handle this by passing them as copy-on-write?
Edit: I guess I could pass the slice as a pointer, but I'm still wondering about the copy-on-write behavior (if it exists) and whether, in general, I should worry about passing by value or by pointer.
Judging from the name of your function, performance can't be that critical to even consider moving parameters to global variables just to save time/space required to pass them as parameters (IO operations like checking files are much-much slower than calling functions and passing values to them).
Slices in Go are just small descriptors, something like a struct with a pointer to a backing array and 2 ints, a length and capacity. No matter how big the backing array is, passing slices are always efficient and you shouldn't even consider passing a pointer to them, unless you want to modify the slice header of course.
Parameters in Go are always passed by value, and a copy of the value being passed is made. If you pass a pointer, then the pointer value will be copied and passed. When a slice is passed, the slice value (which is a small descriptor) will be copied and passed - which will point to the same backing array (which will not be copied).
Also if you need to access the slice multiple times in the function, a parameter is usually an extra gain as compilers can make further optimization / caching, while if it is a global variable, more care has to be taken.
More about slices and their internals: Go Slices: usage and internals
And if you want exact numbers on performance, benchmark!
Here comes a little benchmarking code which shows no difference between the 2 solutions (passing slice as argument or accessing a global slice). Save it into a file like slices_test.go and run it with go test -bench .
package main
import (
"testing"
)
var gslice = make([]string, 1000)
func global(s string) {
for i := 0; i < 100; i++ { // Cycle to access slice may times
_ = s
_ = gslice // Access global-slice
}
}
func param(s string, ss []string) {
for i := 0; i < 100; i++ { // Cycle to access slice may times
_ = s
_ = ss // Access parameter-slice
}
}
func BenchmarkParameter(b *testing.B) {
for i := 0; i < b.N; i++ {
param("hi", gslice)
}
}
func BenchmarkGlobal(b *testing.B) {
for i := 0; i < b.N; i++ {
global("hi")
}
}
Example output:
testing: warning: no tests to run
PASS
BenchmarkParameter-2 30000000 55.4 ns/op
BenchmarkGlobal-2 30000000 55.1 ns/op
ok _/V_/workspace/IczaGo/src/play 3.569s
Piggybacking on #icza's excellent answer, there is another way to pass a slice as parameter: a pointer to a slice.
When you need to modify the underlying slice, the global variable slice works, but passing it as a parameter does not work, you are effectively working with a copy. To mitigate that, one can actually pass the slice as a pointer.
Interesting enough, it's actually faster than accessing a global variable:
package main
import (
"testing"
)
var gslice = make([]string, 1000000)
func global(s string) {
for i := 0; i < 100; i++ { // Cycle to access slice may times
_ = s
_ = gslice // Access global-slice
}
}
func param(s string, ss []string) {
for i := 0; i < 100; i++ { // Cycle to access slice may times
_ = s
_ = ss // Access parameter-slice
}
}
func paramPointer(s string, ss *[]string) {
for i := 0; i < 100; i++ { // Cycle to access slice may times
_ = s
_ = ss // Access parameter-slice
}
}
func BenchmarkParameter(b *testing.B) {
for i := 0; i < b.N; i++ {
param("hi", gslice)
}
}
func BenchmarkParameterPointer(b *testing.B) {
for i := 0; i < b.N; i++ {
paramPointer("hi", &gslice)
}
}
func BenchmarkGlobal(b *testing.B) {
for i := 0; i < b.N; i++ {
global("hi")
}
}
Results:
goos: darwin
goarch: amd64
pkg: untitled
BenchmarkParameter-8 24437403 48.2 ns/op
BenchmarkParameterPointer-8 27483115 40.3 ns/op
BenchmarkGlobal-8 25631470 46.0 ns/op
I rewrote the benchmark so you can compare the results.
As you can see the ParameterPointer bench start to get ahead after 10 records. Which is very interesting.
package slices_bench
import (
"testing"
)
var gslice = make([]string, 1000)
func global(s string) {
for i := 0; i < 100; i++ { // Cycle to access slice may times
_ = s
_ = gslice // Access global-slice
}
}
func param(s string, ss []string) {
for i := 0; i < 100; i++ { // Cycle to access slice may times
_ = s
_ = ss // Access parameter-slice
}
}
func paramPointer(s string, ss *[]string) {
for i := 0; i < 100; i++ { // Cycle to access slice may times
_ = s
_ = ss // Access parameter-slice
}
}
func BenchmarkPerformance(b *testing.B){
fixture := []struct {
desc string
records int
}{
{
desc: "1 record",
records: 1,
},
{
desc: "10 records",
records: 10,
},
{
desc: "100 records",
records: 100,
},
{
desc: "1000 records",
records: 1000,
},
{
desc: "10000 records",
records: 10000,
},
{
desc: "100000 records",
records: 100000,
},
}
tests := []struct {
desc string
fn func(b *testing.B, n int)
}{
{
desc: "ParameterPointer",
fn: func(b *testing.B, n int) {
for j := 0; j < n; j++ {
paramPointer("hi", &gslice)
}
},
},
{
desc: "Parameter",
fn: func(b *testing.B, n int) {
for j := 0; j < n; j++ {
param("hi", gslice)
}
},
},
{
desc: "Global",
fn: func(b *testing.B, n int) {
for j := 0; j < n; j++ {
global("hi")
}
},
},
}
for _, t := range tests {
b.Run(t.desc, func(b *testing.B) {
for _, f := range fixture {
b.Run(f.desc, func(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
t.fn(b, f.records)
}
})
}
})
}
}
Results:
goos: windows
goarch: amd64
pkg: benchs/slices-bench
cpu: Intel(R) Core(TM) i7-10700 CPU # 2.90GHz
BenchmarkPerformance
BenchmarkPerformance/ParameterPointer
BenchmarkPerformance/ParameterPointer/1_record
BenchmarkPerformance/ParameterPointer/1_record-16 38661910 31.18 ns/op 0 B/op 0 allocs/op
BenchmarkPerformance/ParameterPointer/10_records
BenchmarkPerformance/ParameterPointer/10_records-16 4160023 288.4 ns/op 0 B/op 0 allocs/op
BenchmarkPerformance/ParameterPointer/100_records
BenchmarkPerformance/ParameterPointer/100_records-16 445131 2748 ns/op 0 B/op 0 allocs/op
BenchmarkPerformance/ParameterPointer/1000_records
BenchmarkPerformance/ParameterPointer/1000_records-16 43876 27380 ns/op 0 B/op 0 allocs/op
BenchmarkPerformance/ParameterPointer/10000_records
BenchmarkPerformance/ParameterPointer/10000_records-16 4441 273922 ns/op 0 B/op 0 allocs/op
BenchmarkPerformance/ParameterPointer/100000_records
BenchmarkPerformance/ParameterPointer/100000_records-16 439 2739282 ns/op 0 B/op 0 allocs/op
BenchmarkPerformance/Parameter
BenchmarkPerformance/Parameter/1_record
BenchmarkPerformance/Parameter/1_record-16 39860619 30.79 ns/op 0 B/op 0 allocs/op
BenchmarkPerformance/Parameter/10_records
BenchmarkPerformance/Parameter/10_records-16 4152728 288.6 ns/op 0 B/op 0 allocs/op
BenchmarkPerformance/Parameter/100_records
BenchmarkPerformance/Parameter/100_records-16 445634 2757 ns/op 0 B/op 0 allocs/op
BenchmarkPerformance/Parameter/1000_records
BenchmarkPerformance/Parameter/1000_records-16 43618 27496 ns/op 0 B/op 0 allocs/op
BenchmarkPerformance/Parameter/10000_records
BenchmarkPerformance/Parameter/10000_records-16 4450 273960 ns/op 0 B/op 0 allocs/op
BenchmarkPerformance/Parameter/100000_records
BenchmarkPerformance/Parameter/100000_records-16 435 2739053 ns/op 0 B/op 0 allocs/op
BenchmarkPerformance/Global
BenchmarkPerformance/Global/1_record
BenchmarkPerformance/Global/1_record-16 38813095 30.97 ns/op 0 B/op 0 allocs/op
BenchmarkPerformance/Global/10_records
BenchmarkPerformance/Global/10_records-16 4148433 288.4 ns/op 0 B/op 0 allocs/op
BenchmarkPerformance/Global/100_records
BenchmarkPerformance/Global/100_records-16 429274 2758 ns/op 0 B/op 0 allocs/op
BenchmarkPerformance/Global/1000_records
BenchmarkPerformance/Global/1000_records-16 43591 27412 ns/op 0 B/op 0 allocs/op
BenchmarkPerformance/Global/10000_records
BenchmarkPerformance/Global/10000_records-16 4521 274420 ns/op 0 B/op 0 allocs/op
BenchmarkPerformance/Global/100000_records
BenchmarkPerformance/Global/100000_records-16 436 2751042 ns/op 0 B/op 0 allocs/op

How to convert a slice of one numeric type to another type

I'm experimenting with the Go language and am quite new to it. I have successfully gone through the tutorials and am now writing a little program to evaluate its performance for the type of operations that I typically do. I have a lengthy slice of float32 type and need to convert it to a slice of type float64 as efficiently as possible. Other than iterating through the elements of the slice and explicitly converting types of individual elements via output[i] = float64(data[i]), is there method that I can use to convert the entire slice without need for iteration? I've tried searching for a solution but have not found anything directly related.
Go is quite low-level, this means that iterating through the slice is the most efficient method. Other languages may have built-in functions for such things, but all they do is iterate through the slice, there is no way to do it without the iteration. But there are some tricks, specifically use range and avoid indexing the slice as there is overhead in the out of bounds check. This would be the most efficient:
func convertTo64(ar []float32) []float64 {
newar := make([]float64, len(ar))
var v float32
var i int
for i, v = range ar {
newar[i] = float64(v)
}
return newar
}
slice32 := make([]float32, 1000)
slice64 := convertTo64(slice32)
Note that the use of := in the range loop would be inefficient because in the current version of Go the variable is thrown away and recreated each time instead of being reused. Using range instead of for i=0; i<n; i++ is more efficient because it saves bounds checks on ar.
It's always wise to be skeptical of performance claims. For example, "the use of := in the range loop would be inefficient because ... the variable is thrown away and recreated each time instead of being reused."
Let's look at benchmark results from three successive runs.
floats_test.go:
package main
import "testing"
var slice64 []float64
func FuncVar(f32 []float32) []float64 {
f64 := make([]float64, len(f32))
var f float32
var i int
for i, f = range f32 {
f64[i] = float64(f)
}
return f64
}
func BenchmarkFuncVar(b *testing.B) {
f32 := make([]float32, 1024)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
slice64 = FuncVar(f32)
}
}
func RangeVar(f32 []float32) []float64 {
f64 := make([]float64, len(f32))
for i, f := range f32 {
f64[i] = float64(f)
}
return f64
}
func BenchmarkRangeVar(b *testing.B) {
f32 := make([]float32, 1024)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
slice64 = RangeVar(f32)
}
}
func main() {}
Output:
$ go test -v -run=! -bench=.
testing: warning: no tests to run
PASS
BenchmarkFuncVar 100000 12260 ns/op 8192 B/op 1 allocs/op
BenchmarkRangeVar 100000 12125 ns/op 8192 B/op 1 allocs/op
ok so/test 2.703s
$ go test -v -run=! -bench=.
testing: warning: no tests to run
PASS
BenchmarkFuncVar 100000 12620 ns/op 8192 B/op 1 allocs/op
BenchmarkRangeVar 100000 12623 ns/op 8192 B/op 1 allocs/op
ok so/test 2.782s
$ go test -v -run=! -bench=.
testing: warning: no tests to run
PASS
BenchmarkFuncVar 100000 12730 ns/op 8192 B/op 1 allocs/op
BenchmarkRangeVar 100000 12971 ns/op 8192 B/op 1 allocs/op
ok so/test 2.852s
$
As requested by a comment that has been deleted. Here's a benchmark using just the system time.
package main
import (
"fmt"
"time"
)
const N = 1e6
var f32 = make([]float32, 1024)
var slice64 []float64
func FuncVar(f32 []float32) []float64 {
f64 := make([]float64, len(f32))
var f float32
var i int
for i, f = range f32 {
f64[i] = float64(f)
}
return f64
}
func BenchmarkFuncVar() {
t1 := time.Now()
for i := 0; i < N; i++ {
slice64 = FuncVar(f32)
}
t2 := time.Now()
fmt.Println("FuncVar", t2.Sub(t1))
}
func RangeVar(f32 []float32) []float64 {
f64 := make([]float64, len(f32))
for i, f := range f32 {
f64[i] = float64(f)
}
return f64
}
func BenchmarkRangeVar() {
t1 := time.Now()
for i := 0; i < N; i++ {
slice64 = RangeVar(f32)
}
t2 := time.Now()
fmt.Println("RangeVar", t2.Sub(t1))
}
func main() {
BenchmarkFuncVar()
BenchmarkRangeVar()
}
Output:
$ go build floata.go && ./floata
FuncVar 10.479653966s
RangeVar 10.208178244s
$ go build floata.go && ./floata
FuncVar 10.123357283s
RangeVar 10.173007394s
$ go build floata.go && ./floata
FuncVar 9.935580721s
RangeVar 10.109644784s
$ go build floata.go && ./floata
FuncVar 10.070552761s
RangeVar 10.317730473s
$ go build floata.go && ./floata
FuncVar 10.075578601s
RangeVar 10.012273678s
$

Minimum value of set in idiomatic Go

How do I write a function that returns the minimum value of a set in go? I am not just looking for a solution (I know I could just initialize the min value when iterating over the first element and then set a boolean variable that I initialized the min value) but rather an idiomatic solution. Since go doesn't have native sets, assume we have a map[Cell]bool.
Maps are the idiomatic way to implement sets in Go. Idiomatic code uses either bool or struct{} as the map's value type. The latter uses less storage, but requires a little more typing at the keyboard to use.
Assuming that the maximum value for a cell is maxCell, then this function will compute the min:
func min(m map[Cell]bool) Cell {
min := maxCell
for k := range m {
if k < min {
min = k
}
}
return min
}
If Cell is a numeric type, then maxCell can be set to one of the math constants.
Any solution using a map will require a loop over the keys.
You can keep a heap in addition to the map to find a minimum. This will require more storage and code, but can be more efficient depending on the size of the set and how often the minimum function is called.
A different approach and depending on how big your set is, using a self-sorting-slice can be more efficient:
type Cell uint64
type CellSet struct {
cells []Cell
}
func (cs *CellSet) Len() int {
return len(cs.cells)
}
func (cs *CellSet) Swap(i, j int) {
cs.cells[i], cs.cells[j] = cs.cells[j], cs.cells[i]
}
func (cs *CellSet) Less(i, j int) bool {
return cs.cells[i] < cs.cells[j]
}
func (cs *CellSet) Add(c Cell) {
for _, v := range cs.cells {
if v == c {
return
}
}
cs.cells = append(cs.cells, c)
sort.Sort(cs)
}
func (cs *CellSet) Min() Cell {
if cs.Len() > 0 {
return cs.cells[0]
}
return 0
}
func (cs *CellSet) Max() Cell {
if l := cs.Len(); l > 0 {
return cs.cells[l-1]
}
return ^Cell(0)
}
playground // this is a test file, copy it to set_test.go and run go test -bench=. -benchmem -v
BenchmarkSlice 20 75385089 ns/op 104 B/op 0 allocs/op
BenchmarkMap 20 77541424 ns/op 158 B/op 0 allocs/op
BenchmarkSliceAndMin 20 77155563 ns/op 104 B/op 0 allocs/op
BenchmarkMapAndMin 1 1827782378 ns/op 2976 B/op 8 allocs/op

Resources