From c1d7016827689935b47e5d65be7501505a28c501 Mon Sep 17 00:00:00 2001 From: Kwame Akuamoah-Boateng Date: Thu, 25 Jul 2024 22:13:46 +0000 Subject: [PATCH 1/4] Add benchmarks ## Description This MR adds benchmarks to this package using golang's benchmarking tool, making it easier to compare and contrast the differences in speed. This replaces the benchmarks in measurements.go as it provides a cleaner output. --- README.md | 15 +++++++++++++-- gorder_test.go | 35 ++++++++++++++++++++++++++++++++++- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7c14d98..7914230 100644 --- a/README.md +++ b/README.md @@ -16,12 +16,23 @@ Solution (Kahn): [1 {3.141592653589793} 3 5 4] Solution (DFS-based): [1 {3.141592653589793} 3 5 4] ``` +## Benchmarking +To benchmark the different algorithms, run the command below. +```sh +go test ./... -bench=. -run=xxx +``` +You can add the flag `-benchmem` if you want to see the memory allocations. +```sh +go test ./... -bench=. -run=xxx -benchmem +``` + + ## Notes: * Maps are one of the common ways to store graphs in Go. The `TopologicalSort` function input supports `map[interface{}][]interface{}` maps. -* Sub-package `dagenerator` was developed and used for generating random DAGs for performance tests (`cmd/measurements` should be rewritten as benchmarks). +* Sub-package `dagenerator` was developed and used for generating random DAGs for performance tests. * Implementation of Kahn's algorithm does pre-computation in the beginning of its work in order to calculate indegree of every vertex in the graph. This may be split into two separate algorithms/functions in the future: - one that doesn't take any additional input but the graph (current implementation) - - and one that takes additional input of indegrees \ No newline at end of file + - and one that takes additional input of indegrees diff --git a/gorder_test.go b/gorder_test.go index 846a20b..e4d17c9 100644 --- a/gorder_test.go +++ b/gorder_test.go @@ -1,6 +1,10 @@ package gorder -import "testing" +import ( + "testing" + + "github.com/amwolff/gorder/dagenerator" +) func TestTopologicalSort(t *testing.T) { digraph := map[interface{}][]interface{}{ @@ -51,3 +55,32 @@ func TestTopologicalSort(t *testing.T) { t.Fatal("DFS-based: should have returned an error") } } + +func BenchmarkDFSBasedSort(b *testing.B) { + digraph := dagenerator.Generate(10, 50, 30, 50, 30) + b.Run("ultrasuperfast", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _, err := TopologicalSort(digraph, "ultrasuperfast") + if err == nil { + b.Fatal("TopologicalSort: should have returned an error") + } + } + }) + + b.Run("Kahn", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _, err := TopologicalSort(digraph, "kahn") + if err != nil { + b.Errorf("Kahn: %v", err) + } + } + }) + b.Run("dfs based", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _, err := TopologicalSort(digraph, "dfsbased") + if err != nil { + b.Errorf("DFS-based: %v", err) + } + } + }) +} From 98097a2985de0313af74d50ad4cc40da6a11a75c Mon Sep 17 00:00:00 2001 From: Kwame Akuamoah-Boateng Date: Thu, 25 Jul 2024 22:41:58 +0000 Subject: [PATCH 2/4] Rename benchmark to topological sort --- gorder_test.go | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/gorder_test.go b/gorder_test.go index e4d17c9..b2a8f3e 100644 --- a/gorder_test.go +++ b/gorder_test.go @@ -56,18 +56,9 @@ func TestTopologicalSort(t *testing.T) { } } -func BenchmarkDFSBasedSort(b *testing.B) { +func BenchmarkTopologicalSort(b *testing.B) { digraph := dagenerator.Generate(10, 50, 30, 50, 30) - b.Run("ultrasuperfast", func(b *testing.B) { - for i := 0; i < b.N; i++ { - _, err := TopologicalSort(digraph, "ultrasuperfast") - if err == nil { - b.Fatal("TopologicalSort: should have returned an error") - } - } - }) - - b.Run("Kahn", func(b *testing.B) { + b.Run("kahn", func(b *testing.B) { for i := 0; i < b.N; i++ { _, err := TopologicalSort(digraph, "kahn") if err != nil { From e130981e9aefd759acfbcdb7325764cd4d253ee2 Mon Sep 17 00:00:00 2001 From: Kwame Akuamoah-Boateng Date: Wed, 11 Dec 2024 21:08:17 +0000 Subject: [PATCH 3/4] Add generics ## Description This MR makes use of go generics which provides a level of error checking since we can now put constraints on our interfaces. Example, we can limit our map keys to comparable values. --- go.mod | 2 +- gorder.go | 34 +++++++++++++++++----------------- gorder_test.go | 18 +++++++++--------- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/go.mod b/go.mod index 0c34830..b531060 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,5 @@ module github.com/amwolff/gorder -go 1.16 +go 1.20 require github.com/davecgh/go-spew v1.1.1 diff --git a/gorder.go b/gorder.go index 4bad6e4..06bce74 100644 --- a/gorder.go +++ b/gorder.go @@ -5,7 +5,7 @@ import ( "regexp" ) -func TopologicalSort(digraph map[interface{}][]interface{}, algorithm string) (solution []interface{}, err error) { +func TopologicalSort[T comparable, V []T](digraph map[T]V, algorithm string) (solution V, err error) { kahnRgxp, err := regexp.Compile(`[Kk]ahn\z`) if err != nil { return nil, err @@ -29,24 +29,24 @@ func TopologicalSort(digraph map[interface{}][]interface{}, algorithm string) (s return solution, nil } -func kahn(digraph map[interface{}][]interface{}) ([]interface{}, error) { - indegrees := make(map[interface{}]int) - for u := range digraph { - if digraph[u] != nil { - for _, v := range digraph[u] { - indegrees[v]++ - } +func kahn[T comparable, V []T](digraph map[T]V) (V, error) { + indegrees := make(map[T]int) + + // loop through all diagraph and add increase indegrees of values + for _, iter := range digraph { + for _, v := range iter { + indegrees[v]++ } } - var queue []interface{} + var queue V for u := range digraph { if _, ok := indegrees[u]; !ok { queue = append(queue, u) } } - var order []interface{} + var order V for len(queue) > 0 { u := queue[len(queue)-1] queue = queue[:(len(queue) - 1)] @@ -67,16 +67,16 @@ func kahn(digraph map[interface{}][]interface{}) ([]interface{}, error) { return order, nil } -func dfsBased(digraph map[interface{}][]interface{}) ([]interface{}, error) { +func dfsBased[T comparable, V []T](digraph map[T]V) (V, error) { var ( acyclic = true - order []interface{} - permanentMark = make(map[interface{}]bool) - temporaryMark = make(map[interface{}]bool) - visit func(interface{}) + order V + permanentMark = make(map[T]bool) + temporaryMark = make(map[T]bool) + visit func(T) ) - visit = func(u interface{}) { + visit = func(u T) { if temporaryMark[u] { acyclic = false } else if !(temporaryMark[u] || permanentMark[u]) { @@ -89,7 +89,7 @@ func dfsBased(digraph map[interface{}][]interface{}) ([]interface{}, error) { } delete(temporaryMark, u) permanentMark[u] = true - order = append([]interface{}{u}, order...) + order = append(V{u}, order...) } } diff --git a/gorder_test.go b/gorder_test.go index b2a8f3e..6093f3b 100644 --- a/gorder_test.go +++ b/gorder_test.go @@ -7,10 +7,10 @@ import ( ) func TestTopologicalSort(t *testing.T) { - digraph := map[interface{}][]interface{}{ - 1: []interface{}{2, 4}, - 2: []interface{}{3, 5}, - 3: []interface{}{4, 5}, + digraph := map[int][]int{ + 1: {2, 4}, + 2: {3, 5}, + 3: {4, 5}, } want := []int{1, 2, 3, 5, 4} @@ -40,11 +40,11 @@ func TestTopologicalSort(t *testing.T) { } } - graphWithCycles := map[interface{}][]interface{}{ - 1: []interface{}{2, 4}, - 2: []interface{}{3, 5}, - 3: []interface{}{4, 5}, - 4: []interface{}{2}, + graphWithCycles := map[int][]int{ + 1: {2, 4}, + 2: {3, 5}, + 3: {4, 5}, + 4: {2}, } _, err = TopologicalSort(graphWithCycles, "kahn") if err == nil { From 27c957934ed0f751033b88ab92c72acb8e999d64 Mon Sep 17 00:00:00 2001 From: Kwame Akuamoah-Boateng Date: Wed, 11 Dec 2024 21:23:58 +0000 Subject: [PATCH 4/4] Use enums instead of raw string values ## Description This changes the use of raw strings to enums. With this users aware of their algorithm options and can choose between them without having to guess which ones have been implemented. --- cmd/example-2/example_2.go | 4 ++-- cmd/example/example.go | 4 ++-- cmd/measurements/measurements.go | 4 ++-- gorder.go | 30 ++++++++++++++++++------------ gorder_test.go | 15 ++++++++------- 5 files changed, 32 insertions(+), 25 deletions(-) diff --git a/cmd/example-2/example_2.go b/cmd/example-2/example_2.go index 0c6dd8b..5c648b6 100644 --- a/cmd/example-2/example_2.go +++ b/cmd/example-2/example_2.go @@ -37,13 +37,13 @@ func main() { 3: []int{4, 5}, } - output, err = gorder.TopologicalSort(serialize(digraph), "kahn") + output, err = gorder.TopologicalSort(serialize(digraph), gorder.KAHN) if err != nil { log.Fatal(err) } fmt.Printf("Solution (Kahn): %d\n", deserialize(output)) - output, err = gorder.TopologicalSort(serialize(digraph), "dfsbased") + output, err = gorder.TopologicalSort(serialize(digraph), gorder.DFS) if err != nil { log.Fatal(err) } diff --git a/cmd/example/example.go b/cmd/example/example.go index e3902c8..64c5bd5 100644 --- a/cmd/example/example.go +++ b/cmd/example/example.go @@ -23,13 +23,13 @@ func main() { 3: []interface{}{"4", "5"}, } - o, err = gorder.TopologicalSort(digraph, "kahn") + o, err = gorder.TopologicalSort(digraph, gorder.KAHN) if err != nil { log.Fatal(err) } fmt.Printf("Solution (Kahn): %v\n", o) - o, err = gorder.TopologicalSort(digraph, "dfsbased") + o, err = gorder.TopologicalSort(digraph, gorder.DFS) if err != nil { log.Fatal(err) } diff --git a/cmd/measurements/measurements.go b/cmd/measurements/measurements.go index c02466f..8491d39 100644 --- a/cmd/measurements/measurements.go +++ b/cmd/measurements/measurements.go @@ -15,7 +15,7 @@ func main() { log.Printf("DAG generation time: %v", time.Since(start)) start = time.Now() - s, err := gorder.TopologicalSort(graph, "kahn") + s, err := gorder.TopologicalSort(graph, gorder.KAHN) log.Printf("Kahn resolve time: %v", time.Since(start)) if err != nil { log.Fatal(err) @@ -23,7 +23,7 @@ func main() { spew.Dump(s) start = time.Now() - s, err = gorder.TopologicalSort(graph, "dfsbased") + s, err = gorder.TopologicalSort(graph, gorder.DFS) log.Printf("DFS-based resolve time: %v", time.Since(start)) if err != nil { log.Fatal(err) diff --git a/gorder.go b/gorder.go index 06bce74..6407155 100644 --- a/gorder.go +++ b/gorder.go @@ -2,24 +2,30 @@ package gorder import ( "errors" - "regexp" ) -func TopologicalSort[T comparable, V []T](digraph map[T]V, algorithm string) (solution V, err error) { - kahnRgxp, err := regexp.Compile(`[Kk]ahn\z`) - if err != nil { - return nil, err - } - dfsBasedRgxp, err := regexp.Compile(`[Dd][Ff][Ss]-?[Bb]ased\z`) - if err != nil { - return nil, err - } +type algo int + +const ( + DFS algo = iota + KAHN +) + +func TopologicalSort[T comparable, V []T](digraph map[T]V, algorithm algo) (solution V, err error) { + // kahnRgxp, err := regexp.Compile(`[Kk]ahn\z`) + // if err != nil { + // return nil, err + // } + // dfsBasedRgxp, err := regexp.Compile(`[Dd][Ff][Ss]-?[Bb]ased\z`) + // if err != nil { + // return nil, err + // } - if kahnRgxp.MatchString(algorithm) { + if algorithm == KAHN { if solution, err = kahn(digraph); err != nil { return nil, err } - } else if dfsBasedRgxp.MatchString(algorithm) { + } else if algorithm == DFS { if solution, err = dfsBased(digraph); err != nil { return nil, err } diff --git a/gorder_test.go b/gorder_test.go index 6093f3b..35ec941 100644 --- a/gorder_test.go +++ b/gorder_test.go @@ -15,12 +15,13 @@ func TestTopologicalSort(t *testing.T) { want := []int{1, 2, 3, 5, 4} - _, err := TopologicalSort(digraph, "ultrasuperfast") + + _, err := TopologicalSort(digraph, 3) if err == nil { t.Fatal("TopologicalSort: should have returned an error") } - o, err := TopologicalSort(digraph, "kahn") + o, err := TopologicalSort(digraph, KAHN) if err != nil { t.Fatalf("Kahn: %v", err) } @@ -30,7 +31,7 @@ func TestTopologicalSort(t *testing.T) { } } - o, err = TopologicalSort(digraph, "dfsbased") + o, err = TopologicalSort(digraph, DFS) if err != nil { t.Fatalf("DFS-based: %v", err) } @@ -46,11 +47,11 @@ func TestTopologicalSort(t *testing.T) { 3: {4, 5}, 4: {2}, } - _, err = TopologicalSort(graphWithCycles, "kahn") + _, err = TopologicalSort(graphWithCycles, KAHN) if err == nil { t.Fatal("Kahn: should have returned an error") } - _, err = TopologicalSort(graphWithCycles, "dfsbased") + _, err = TopologicalSort(graphWithCycles, DFS) if err == nil { t.Fatal("DFS-based: should have returned an error") } @@ -60,7 +61,7 @@ func BenchmarkTopologicalSort(b *testing.B) { digraph := dagenerator.Generate(10, 50, 30, 50, 30) b.Run("kahn", func(b *testing.B) { for i := 0; i < b.N; i++ { - _, err := TopologicalSort(digraph, "kahn") + _, err := TopologicalSort(digraph, KAHN) if err != nil { b.Errorf("Kahn: %v", err) } @@ -68,7 +69,7 @@ func BenchmarkTopologicalSort(b *testing.B) { }) b.Run("dfs based", func(b *testing.B) { for i := 0; i < b.N; i++ { - _, err := TopologicalSort(digraph, "dfsbased") + _, err := TopologicalSort(digraph, DFS) if err != nil { b.Errorf("DFS-based: %v", err) }