Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change split function from largest axis to minimum overlap #6

Closed
wants to merge 2 commits into from

Conversation

cep-ter
Copy link

@cep-ter cep-ter commented Apr 7, 2022

The current splitting mechanism seems quite simple and based on recent papers on node splitting for r-trees I think there is a lot of room for improving it.

One way is instead of splitting the rectangle (when it reached maximum entries) based on its largest axis
we can split it based on the axis which makes a smaller overlapping area after the split.

This way it generates more efficient splitting because the smaller the overlapping area helps find the right rectangle faster.
When there is no overlapping area (the overlapping area is zero) we can use the largest axis like before.

I run this benchmark from the test file for 10 million records for 5 times go test -bench=. -run=TestGeoIndex -count=5
which gives these results.

Largest axis:

seed: 1649339938841388500
insert: 10,000,000 ops in 9713ms, 1,029,571/sec, 971 ns/op
search: 10,000,000 ops in 11043ms, 905,545/sec, 1104 ns/op
delete: 10,000,000 ops in 18059ms, 553,752/sec, 1805 ns/op
insert: 10,000,000 ops in 9486ms, 1,054,226/sec, 948 ns/op
search: 10,000,000 ops in 13304ms, 751,670/sec, 1330 ns/op
delete: 10,000,000 ops in 14877ms, 672,192/sec, 1487 ns/op
insert: 10,000,000 ops in 8545ms, 1,170,221/sec, 854 ns/op
search: 10,000,000 ops in 8024ms, 1,246,227/sec, 802 ns/op
delete: 10,000,000 ops in 13623ms, 734,071/sec, 1362 ns/op
insert: 10,000,000 ops in 9244ms, 1,081,823/sec, 924 ns/op
search: 10,000,000 ops in 8730ms, 1,145,484/sec, 872 ns/op
delete: 10,000,000 ops in 14045ms, 711,980/sec, 1404 ns/op
insert: 10,000,000 ops in 8570ms, 1,166,880/sec, 856 ns/op
search: 10,000,000 ops in 9173ms, 1,090,108/sec, 917 ns/op
delete: 10,000,000 ops in 14159ms, 706,288/sec, 1415 ns/op
goos: linux
goarch: amd64
pkg: github.com/tidwall/rtree
cpu: Intel(R) Core(TM) i7-10510U CPU @ 1.80GHz
BenchmarkRandomInsert-8 2106502 662.1 ns/op
BenchmarkRandomInsert-8 2076409 649.2 ns/op
BenchmarkRandomInsert-8 1785523 671.1 ns/op
BenchmarkRandomInsert-8 1993303 698.6 ns/op
BenchmarkRandomInsert-8 1968975 668.6 ns/op
PASS
ok github.com/tidwall/rtree 187.575s

Minimum overlapping:

seed: 1649340172027585044
insert: 10,000,000 ops in 10305ms, 970,424/sec, 1030 ns/op
search: 10,000,000 ops in 8469ms, 1,180,722/sec, 846 ns/op
delete: 10,000,000 ops in 17717ms, 564,439/sec, 1771 ns/op
insert: 10,000,000 ops in 9831ms, 1,017,192/sec, 983 ns/op
search: 10,000,000 ops in 7829ms, 1,277,341/sec, 782 ns/op
delete: 10,000,000 ops in 13622ms, 734,104/sec, 1362 ns/op
insert: 10,000,000 ops in 8415ms, 1,188,372/sec, 841 ns/op
search: 10,000,000 ops in 7520ms, 1,329,788/sec, 751 ns/op
delete: 10,000,000 ops in 12974ms, 770,768/sec, 1297 ns/op
insert: 10,000,000 ops in 9586ms, 1,043,225/sec, 958 ns/op
search: 10,000,000 ops in 9120ms, 1,096,486/sec, 912 ns/op
delete: 10,000,000 ops in 14197ms, 704,351/sec, 1419 ns/op
insert: 10,000,000 ops in 9437ms, 1,059,663/sec, 943 ns/op
search: 10,000,000 ops in 7609ms, 1,314,176/sec, 760 ns/op
delete: 10,000,000 ops in 13316ms, 750,948/sec, 1331 ns/op
goos: linux
goarch: amd64
pkg: github.com/tidwall/rtree
cpu: Intel(R) Core(TM) i7-10510U CPU @ 1.80GHz
BenchmarkRandomInsert-8 1834258 735.7 ns/op
BenchmarkRandomInsert-8 1822066 719.2 ns/op
BenchmarkRandomInsert-8 1780968 716.1 ns/op
BenchmarkRandomInsert-8 1821146 765.8 ns/op
BenchmarkRandomInsert-8 1800307 716.3 ns/op
PASS
ok github.com/tidwall/rtree 176.871s

Searching took about 810 ns/op for minimum_overlap and 1005 ns/op for the largest axis which seems a fairly good imporvment.

Insertion time increased a bit from about 669 ns/op to about 730 ns/op because we do more jobs in the splitting phase.

I think in geo indexing people insert the data once and query it many times so this algorithm may speed up queries for most of the users.

Instead of spliting rectangle (when it reached maximum entries) based on it's largest axis
we can split it based on the axis which make smaller overlaping area after split.
@tidwall
Copy link
Owner

tidwall commented Apr 7, 2022

I appreciate you looking for extra speed enhancements to my R-tree algorithm.

What I noticed when testing your PR is that the resulting R-tree contains node rectangles that have very tall vertical slices.

Running go test on your PR will generate a file named cities.svg, which looks like this:

cities

What I generally look for is something that looks more like this:

cities

Seeing those tall slices usually indicates that the searches will be slow.

I think the reason that your PR is displaying faster searches is because the benchmark is designed to specifically search for each item in the tree. So each of those search operations use a minimally sized rectangle that is targeting only one item. And because your change to the split also minimizes overlapping, the overall operation is faster.

But, real world searches are often larger rectangles that target more than one item, and will probably overlap many child rectangles while traversing the tree.

For example, let's say we want to search for all items contained in the following red rectangle:

image

The top tree will scan many more overlapping rectangles than the bottom tree. And will likely take much longer for the search to complete.

Sadly my benchmark suite didn't have tests that represent this very common use case, so I added some new ones moments ago.

Here are the new results with the current R-tree in the master branch:

insert:       1,000,000 ops in 419ms, 2,386,563/sec, 419 ns/op
search-item:  1,000,000 ops in 426ms, 2,349,847/sec, 425 ns/op
search-1%:    10,000 ops in 32ms, 313,042/sec, 3194 ns/op
search-5%:    10,000 ops in 232ms, 43,177/sec, 23160 ns/op
search-10%:   10,000 ops in 727ms, 13,763/sec, 72656 ns/op
delete:       1,000,000 ops in 583ms, 1,714,904/sec, 583 ns/op

And here are the results when those tests are added to this PR:

insert:       1,000,000 ops in 417ms, 2,396,659/sec, 417 ns/op
search-item:  1,000,000 ops in 389ms, 2,571,345/sec, 388 ns/op
search-1%:    10,000 ops in 194ms, 51,507/sec, 19414 ns/op
search-5%:    10,000 ops in 1143ms, 8,748/sec, 114302 ns/op
search-10%:   10,000 ops in 2574ms, 3,884/sec, 257401 ns/op
delete:       1,000,000 ops in 527ms, 1,896,051/sec, 527 ns/op

The search-1%, search-5%, and search-10% performs searches on random rectangles that are 1%, 5%, and 10% the size of the bounds of the tree.

While the insert, insert-item, and delete operations are a little faster, the rectangle searches are much slower. This slowdown will also affect nearest neighbor operations.

@cep-ter cep-ter closed this Sep 30, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants