Skip to content

Commit e6206d7

Browse files
committed
test: ✅ add benchmarking setup to compare against kyroy/kdtree
The benchmarking setup I've added compares the API performance between my library and the `kyroy/kdtree` library, providing valuable insights into the performance characteristics of each one. The APIs compared against each other are listed in the table below: | S. No. | rishitc/go-kd-tree | kyroy/kdtree | |:------:|---------------------|--------------| | 1 | NewKDTreeWithValues | New | | 2 | RangeSearch | RangeSearch | | 3 | Values | Points | | 4 | Insert | Insert | | 5 | Remove | Remove | | 6 | Balance | Balance | | 7 | KNN | KNN | Certain APIs, such as `String,` have not been benchmarked because they are not performance-critical and are primarily used for debugging and educational purposes. Additionally, there is no standard way of representing a tree as a string, so the benchmark results are heavily dependent on your library's chosen format of data representation as a string. In this benchmarking setup, we no longer generate the benchmarking dataset on the fly every time we benchmark. Instead, we generate the benchmarking dataset once and store it in a `.csv` file (if it does not already exist), which is later read and stored in memory before running any benchmarks. This new approach significantly reduces the time taken to run a benchmark. To simplify running the benchmarking setup, which uses build flags, I've included a template command in the file `internal/benchmarks/README.md` that can be easily modified and used to run the benchmarks locally. Finally, I have removed the file used to benchmark old and new approaches for implementing different APIs.
1 parent 2d5cdc4 commit e6206d7

File tree

14 files changed

+448
-93
lines changed

14 files changed

+448
-93
lines changed

.gitignore

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,10 @@ Temporary Items
5757
# iCloud generated files
5858
*.icloud
5959

60-
# End of https://www.toptal.com/developers/gitignore/api/go,macos
60+
# End of https://www.toptal.com/developers/gitignore/api/go,macos
61+
62+
# Ignore generated benchmarking dataset files
63+
benchmarks/dataset/*.csv
64+
65+
# Ignore pkg folder and its contents
66+
/pkg/*

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ go 1.22.5
44

55
require (
66
github.com/google/flatbuffers v24.3.25+incompatible
7+
github.com/kyroy/kdtree v0.0.0-20200419114247-70830f883f1d
78
github.com/stretchr/testify v1.9.0
89
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
910
)
1011

1112
require (
1213
github.com/davecgh/go-spew v1.1.1 // indirect
14+
github.com/kyroy/priority-queue v0.0.0-20180327160706-6e21825e7e0c // indirect
1315
github.com/pmezard/go-difflib v1.0.0 // indirect
1416
gopkg.in/yaml.v3 v3.0.1 // indirect
1517
)

go.sum

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,24 @@
1+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
12
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
23
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
34
github.com/google/flatbuffers v24.3.25+incompatible h1:CX395cjN9Kke9mmalRoL3d81AtFUxJM+yDthflgJGkI=
45
github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
6+
github.com/jupp0r/go-priority-queue v0.0.0-20160601094913-ab1073853bde h1:+5PMaaQtDUwOcJIUlmX89P0J3iwTvErTmyn5WghzXAQ=
7+
github.com/jupp0r/go-priority-queue v0.0.0-20160601094913-ab1073853bde/go.mod h1:RDgD/dfPmIwFH0qdUOjw71HjtWg56CtyLIoHL+R1wJw=
8+
github.com/kyroy/kdtree v0.0.0-20200419114247-70830f883f1d h1:1n5M/49q9H6QtNJiiVL/W5mqgT1UdlGQ7oLP+DkJ1vs=
9+
github.com/kyroy/kdtree v0.0.0-20200419114247-70830f883f1d/go.mod h1:6oJGQK7VSg3RxSQ7QspgqpCmKjIbAslgT2wBXbFJUZw=
10+
github.com/kyroy/priority-queue v0.0.0-20180327160706-6e21825e7e0c h1:1c7+XOOGQ19cXjZ1Ss/irljQxgPvb+8z+jNEprCXl20=
11+
github.com/kyroy/priority-queue v0.0.0-20180327160706-6e21825e7e0c/go.mod h1:R477L6j2/dUcE0q0aftk0kR5Xt93W7g1066AodcJhEo=
512
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
613
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
14+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
15+
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
716
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
817
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
918
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
1019
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
1120
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
1221
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
22+
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
1323
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
1424
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

internal/benchmarks/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Benchmarking Guide
2+
3+
## How to run the benchmarks?
4+
5+
* The benchmarks can be run by using the below command:
6+
* Ensure to fill in the placeholder values in the command with appropriate values before running it.
7+
8+
```bash
9+
go test -benchtime=100x -tags trace -benchmem -run=^$ -bench ^<benchmark_function_name>$ github.com/rishitc/go-kd-tree/benchmarks/<competitor_folder_name>
10+
```
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
//go:build trace
2+
3+
package dataset
4+
5+
import (
6+
"encoding/csv"
7+
"fmt"
8+
"math/rand"
9+
"os"
10+
"strconv"
11+
)
12+
13+
func Trace2DGenerator(tracePath string) {
14+
// Open the file for writing
15+
file, err := os.Create(tracePath)
16+
if err != nil {
17+
panic(err)
18+
}
19+
defer file.Close()
20+
21+
// Create a CSV writer
22+
writer := csv.NewWriter(file)
23+
defer writer.Flush()
24+
25+
// Create a map to store unique pairs
26+
uniquePairs := make(map[string]struct{})
27+
28+
// Generate unique pairs
29+
for len(uniquePairs) < 1000000 {
30+
x := rand.Intn(1000000)
31+
y := rand.Intn(1000000)
32+
pair := fmt.Sprintf("%d,%d", x, y)
33+
34+
if _, exists := uniquePairs[pair]; !exists {
35+
uniquePairs[pair] = struct{}{}
36+
err := writer.Write([]string{strconv.Itoa(x), strconv.Itoa(y)})
37+
if err != nil {
38+
panic(err)
39+
}
40+
}
41+
}
42+
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
//go:test trace
2+
package gokdtree_test
3+
4+
import (
5+
"errors"
6+
"math/rand/v2"
7+
"os"
8+
"path/filepath"
9+
"runtime"
10+
"testing"
11+
12+
kdtree "github.com/rishitc/go-kd-tree"
13+
"github.com/rishitc/go-kd-tree/internal/benchmarks/dataset"
14+
"github.com/rishitc/go-kd-tree/internal/types"
15+
)
16+
17+
const dimensions2DCount = 2
18+
19+
var (
20+
tracePath = filepath.Join("..", "dataset", "input_2d_trace.csv")
21+
trace []types.Tensor2D
22+
)
23+
24+
func init() {
25+
if _, err := os.Stat(tracePath); errors.Is(err, os.ErrNotExist) {
26+
dataset.Trace2DGenerator(tracePath)
27+
}
28+
trace, _ = ReadTensor2DTrace(tracePath)
29+
}
30+
31+
func BenchmarkGoKDTreeCreation(b *testing.B) {
32+
var tree *kdtree.KDTree[types.Tensor2D]
33+
for i := 0; i < b.N; i++ {
34+
tree = kdtree.NewKDTreeWithValues(dimensions2DCount, trace)
35+
}
36+
runtime.KeepAlive(tree)
37+
}
38+
39+
func BenchmarkGoKDTreeInsert(b *testing.B) {
40+
var tree *kdtree.KDTree[types.Tensor2D]
41+
for i := 0; i < b.N; i++ {
42+
b.StopTimer()
43+
t := kdtree.NewKDTreeWithValues(dimensions2DCount, trace[:len(trace)-1])
44+
e := trace[len(trace)-1]
45+
b.StartTimer()
46+
47+
t.Insert(e)
48+
}
49+
runtime.KeepAlive(tree)
50+
}
51+
52+
func BenchmarkGoKDTreeRemove(b *testing.B) {
53+
var point types.Tensor2D
54+
for i := 0; i < b.N; i++ {
55+
b.StopTimer()
56+
tree := kdtree.NewKDTreeWithValues(dimensions2DCount, trace)
57+
// Select random element to remove
58+
ti := rand.IntN(len(trace))
59+
e := trace[ti]
60+
b.StartTimer()
61+
62+
tree.Remove(e)
63+
}
64+
runtime.KeepAlive(point)
65+
}
66+
67+
func BenchmarkGoKDTreeKNN(b *testing.B) {
68+
var points []types.Tensor2D
69+
tree := kdtree.NewKDTreeWithValues(dimensions2DCount, trace)
70+
b.ResetTimer()
71+
72+
for i := 0; i < b.N; i++ {
73+
b.StopTimer()
74+
ti := rand.IntN(len(trace))
75+
e := trace[ti]
76+
b.StartTimer()
77+
78+
points = tree.KNN(e, 100)
79+
}
80+
runtime.KeepAlive(points)
81+
}
82+
83+
func BenchmarkGoKDTreeValues(b *testing.B) {
84+
var points []types.Tensor2D
85+
for i := 0; i < b.N; i++ {
86+
b.StopTimer()
87+
tree := kdtree.NewKDTreeWithValues(dimensions2DCount, trace)
88+
b.StartTimer()
89+
90+
points = tree.Values()
91+
}
92+
runtime.KeepAlive(points)
93+
}
94+
95+
func BenchmarkGoKDTreeRangeSearch(b *testing.B) {
96+
var points []types.Tensor2D
97+
for i := 0; i < b.N; i++ {
98+
b.StopTimer()
99+
tree := kdtree.NewKDTreeWithValues(dimensions2DCount, trace)
100+
b.StartTimer()
101+
102+
points = tree.RangeSearch(func(td types.Tensor2D, i int) kdtree.RelativePosition {
103+
const (
104+
xs = -1
105+
xe = 801
106+
ys = 0
107+
ye = 251
108+
)
109+
switch i {
110+
case -1:
111+
if x, y := td[0], td[1]; xs <= x && x < xe && ys <= y && y < ye {
112+
return kdtree.InRange
113+
}
114+
return kdtree.AfterRange
115+
case 0:
116+
if x := td[0]; x < xs {
117+
return kdtree.BeforeRange
118+
} else if x >= xe {
119+
return kdtree.AfterRange
120+
} else {
121+
return kdtree.InRange
122+
}
123+
case 1:
124+
if y := td[1]; y < ys {
125+
return kdtree.BeforeRange
126+
} else if y >= ye {
127+
return kdtree.AfterRange
128+
} else {
129+
return kdtree.InRange
130+
}
131+
}
132+
return kdtree.AfterRange
133+
})
134+
}
135+
runtime.KeepAlive(points)
136+
}
137+
138+
func BenchmarkGoKDTreeBalance(b *testing.B) {
139+
var tree *kdtree.KDTree[types.Tensor2D]
140+
for i := 0; i < b.N; i++ {
141+
b.StopTimer()
142+
tree = kdtree.NewKDTreeWithValues(dimensions2DCount, []types.Tensor2D{})
143+
for _, e := range trace {
144+
tree.Insert(e)
145+
}
146+
b.StartTimer()
147+
148+
tree.Balance()
149+
}
150+
runtime.KeepAlive(tree)
151+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package gokdtree_test
2+
3+
import (
4+
"github.com/rishitc/go-kd-tree/internal/benchmarks"
5+
"github.com/rishitc/go-kd-tree/internal/types"
6+
)
7+
8+
func ReadTensor2DTrace(filename string) ([]types.Tensor2D, error) {
9+
trace, err := benchmarks.Read2DTrace(filename)
10+
tensorTrace := make([]types.Tensor2D, 0, len(trace))
11+
12+
for _, e := range trace {
13+
tensorTrace = append(tensorTrace, types.Tensor2D(e))
14+
}
15+
16+
return tensorTrace, err
17+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
//go:test trace
2+
package kyro_test
3+
4+
import (
5+
"errors"
6+
"math/rand/v2"
7+
"os"
8+
"path/filepath"
9+
"runtime"
10+
"testing"
11+
12+
"github.com/kyroy/kdtree"
13+
"github.com/kyroy/kdtree/kdrange"
14+
"github.com/rishitc/go-kd-tree/internal/benchmarks/dataset"
15+
)
16+
17+
var (
18+
tracePath = filepath.Join("..", "dataset", "input_2d_trace.csv")
19+
trace []kdtree.Point
20+
)
21+
22+
func init() {
23+
if _, err := os.Stat(tracePath); errors.Is(err, os.ErrNotExist) {
24+
dataset.Trace2DGenerator(tracePath)
25+
}
26+
trace, _ = ReadPoint2DTrace(tracePath)
27+
}
28+
29+
func BenchmarkKyroKDTreeCreation(b *testing.B) {
30+
var tree *kdtree.KDTree
31+
for i := 0; i < b.N; i++ {
32+
tree = kdtree.New(trace)
33+
}
34+
runtime.KeepAlive(tree)
35+
}
36+
37+
func BenchmarkKyroKDTreeInsert(b *testing.B) {
38+
var tree *kdtree.KDTree
39+
for i := 0; i < b.N; i++ {
40+
b.StopTimer()
41+
t := kdtree.New(trace[:len(trace)-1])
42+
e := trace[len(trace)-1]
43+
b.StartTimer()
44+
45+
t.Insert(e)
46+
}
47+
runtime.KeepAlive(tree)
48+
}
49+
50+
func BenchmarkKyroKDTreeRemove(b *testing.B) {
51+
var point kdtree.Point
52+
for i := 0; i < b.N; i++ {
53+
b.StopTimer()
54+
tree := kdtree.New(trace)
55+
// Select random element to remove
56+
ti := rand.IntN(len(trace))
57+
e := trace[ti]
58+
b.StartTimer()
59+
60+
point = tree.Remove(e)
61+
}
62+
runtime.KeepAlive(point)
63+
}
64+
65+
func BenchmarkKyroKDTreeKNN(b *testing.B) {
66+
var points []kdtree.Point
67+
tree := kdtree.New(trace)
68+
b.ResetTimer()
69+
70+
for i := 0; i < b.N; i++ {
71+
b.StopTimer()
72+
ti := rand.IntN(len(trace))
73+
e := trace[ti]
74+
b.StartTimer()
75+
76+
points = tree.KNN(e, 100)
77+
}
78+
runtime.KeepAlive(points)
79+
}
80+
81+
func BenchmarkKyroKDTreePoints(b *testing.B) {
82+
var points []kdtree.Point
83+
for i := 0; i < b.N; i++ {
84+
b.StopTimer()
85+
tree := kdtree.New(trace)
86+
b.StartTimer()
87+
88+
points = tree.Points()
89+
}
90+
runtime.KeepAlive(points)
91+
}
92+
93+
func BenchmarkKyroKDTreeRangeSearch(b *testing.B) {
94+
var points []kdtree.Point
95+
for i := 0; i < b.N; i++ {
96+
b.StopTimer()
97+
tree := kdtree.New(trace)
98+
b.StartTimer()
99+
100+
points = tree.RangeSearch(kdrange.New(-1, 800, 0, 250))
101+
}
102+
runtime.KeepAlive(points)
103+
}
104+
105+
func BenchmarkKyroKDTreeBalance(b *testing.B) {
106+
var tree *kdtree.KDTree
107+
for i := 0; i < b.N; i++ {
108+
b.StopTimer()
109+
tree = kdtree.New([]kdtree.Point{})
110+
for _, e := range trace {
111+
tree.Insert(e)
112+
}
113+
b.StartTimer()
114+
115+
tree.Balance()
116+
}
117+
runtime.KeepAlive(tree)
118+
}

0 commit comments

Comments
 (0)