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

Sorts s_center to improve search #15

Open
wants to merge 13 commits into
base: master
Choose a base branch
from

Conversation

danielpetri1
Copy link

@danielpetri1 danielpetri1 commented Jul 2, 2021

Note: this PR builds upon the work by @amarzot-yesware to allow the use of custom objects, as well as my other PR in which point_search was fixed.

  • Adds a new parameter that defaults to true which controls whether the result is sorted or not, resulting in faster query speeds when set to false.
  • s_center is sorted in divide_intervals as to increase the speed of the point_search method. Specs and search methods modified accordingly.
  • Linted code and removed duplicate method calls

danielpetri1 and others added 4 commits June 28, 2021 16:02
This change implements point_search iteratively to ensure a stack overflow can not occur.

It also makes the method use the passed unique parameter, as the recursive call was previously defaulting unique to true:

itv = [(5...20), (15.6...20), (15.7...20), (15.7...20)]
t = IntervalTree::Tree.new(itv)
p t.search(15.7)     #=> [5...20, 15.6...20, 15.7...20]
p t.search(15.7, unique: false) #=> Should be [5...20, 15.6...20, 15.7...20, 15.7...20], not [5...20, 15.6...20, 15.7...20] again

Additionally, result.uniq is invoked at the end of the method for faster query speeds.
Fixed point_search Indentation
Co-authored-by: amarzot-yesware <[email protected]>
Copy link
Contributor

@ZimbiX ZimbiX left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice one.

Again, could you add a spec please?

And let's rebase this too when ready.

btw, I'd separate linting changes from functionality changes in future to make reviewing easier =)

lib/interval_tree.rb Outdated Show resolved Hide resolved
Copy link
Contributor

@ZimbiX ZimbiX left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I should have selected this, given the missing spec)

lib/interval_tree.rb Outdated Show resolved Hide resolved
@amarzot-yesware
Copy link

amarzot-yesware commented Jul 13, 2021

I decided to benchmark this PR's approach for searching the node. I did this by constructing a node with some number of overlapping intervals (as they would be constructed in the tree) and trying different searching algorithms.

Context

Algorithms Benchmarked

  • search_node refers to the current implementation
  • search_node_sorted refers to the implementation from this PR
  • search_node_bsearch is my attempt at a faster searching algortihm

Timing

Here is an explanation of the types of timings in the output (user, system, etc.) https://gist.github.com/anildigital/1229896

Types of Queries

left query, centered query, and right query refer to how the query interval relates to the x_center of the node. left queries are entirely to the left of the center, centered queries overlap, and right queries are entirely to the right.

                                        x_center
                                            |
|---left query---|      |--centered query---|--|             |---right query---|
                                        |---|--centered query--|
                                            |

I ran the benchmarks with different numbers of intervals in s_center (from 1..10) and with each type of query. Each time is the result of calling the respective function 1,000,000 times. In addition to defining the type of query I also benchmarked with 'benchmark-ips' and random queries to get a better feel.

Results

The results are as follows (with analysis and code afterwards):

Benchmark Query Types
RUBY_VERSION => "3.0.0"
>>> Intervals: 1 ***************************************************************
                           user     system      total        real
>> left query --------------------------------------------------------
search_node        :   0.294283   0.000423   0.294706 (  0.295249)
search_node_sorted :   0.321450   0.000893   0.322343 (  0.323031)
search_node_bsearch:   0.344577   0.000515   0.345092 (  0.345597)
>> centered query ----------------------------------------------------
search_node        :   0.271940   0.000380   0.272320 (  0.273070)
search_node_sorted :   0.318535   0.000614   0.319149 (  0.319852)
search_node_bsearch:   0.367545   0.000615   0.368160 (  0.368673)
>> right query -------------------------------------------------------
search_node        :   0.275326   0.000611   0.275937 (  0.276416)
search_node_sorted :   0.318272   0.000717   0.318989 (  0.319567)
search_node_bsearch:   0.492927   0.000730   0.493657 (  0.494747)
>>> Intervals: 2 ***************************************************************
                           user     system      total        real
>> left query --------------------------------------------------------
search_node        :   0.345919   0.000590   0.346509 (  0.347349)
search_node_sorted :   0.478631   0.000768   0.479399 (  0.480321)
search_node_bsearch:   0.477955   0.000700   0.478655 (  0.479515)
>> centered query ----------------------------------------------------
search_node        :   0.404176   0.000944   0.405120 (  0.406228)
search_node_sorted :   0.497447   0.000973   0.498420 (  0.499066)
search_node_bsearch:   0.366595   0.000562   0.367157 (  0.367870)
>> right query -------------------------------------------------------
search_node        :   0.397719   0.000684   0.398403 (  0.398869)
search_node_sorted :   0.494382   0.000784   0.495166 (  0.495986)
search_node_bsearch:   0.501929   0.000716   0.502645 (  0.503245)
>>> Intervals: 3 ***************************************************************
                           user     system      total        real
>> left query --------------------------------------------------------
search_node        :   0.469805   0.000498   0.470303 (  0.471141)
search_node_sorted :   0.648218   0.001123   0.649341 (  0.650667)
search_node_bsearch:   0.502000   0.000781   0.502781 (  0.503568)
>> centered query ----------------------------------------------------
search_node        :   0.509840   0.000565   0.510405 (  0.510967)
search_node_sorted :   0.644760   0.000841   0.645601 (  0.646534)
search_node_bsearch:   0.368921   0.000554   0.369475 (  0.369940)
>> right query -------------------------------------------------------
search_node        :   0.523597   0.000866   0.524463 (  0.525806)
search_node_sorted :   0.651550   0.000698   0.652248 (  0.652764)
search_node_bsearch:   0.571649   0.000815   0.572464 (  0.573079)
>>> Intervals: 4 ***************************************************************
                           user     system      total        real
>> left query --------------------------------------------------------
search_node        :   0.662972   0.001529   0.664501 (  0.666598)
search_node_sorted :   0.823046   0.001221   0.824267 (  0.825586)
search_node_bsearch:   0.464917   0.000611   0.465528 (  0.466402)
>> centered query ----------------------------------------------------
search_node        :   0.657194   0.001182   0.658376 (  0.659535)
search_node_sorted :   0.831549   0.001223   0.832772 (  0.834295)
search_node_bsearch:   0.377436   0.000624   0.378060 (  0.378636)
>> right query -------------------------------------------------------
search_node        :   0.639606   0.000872   0.640478 (  0.641647)
search_node_sorted :   0.794061   0.001227   0.795288 (  0.796666)
search_node_bsearch:   0.640082   0.001056   0.641138 (  0.642146)
>>> Intervals: 5 ***************************************************************
                           user     system      total        real
>> left query --------------------------------------------------------
search_node        :   0.769376   0.001229   0.770605 (  0.771601)
search_node_sorted :   1.004979   0.001338   1.006317 (  1.008085)
search_node_bsearch:   0.428967   0.000775   0.429742 (  0.430589)
>> centered query ----------------------------------------------------
search_node        :   0.749577   0.001233   0.750810 (  0.751987)
search_node_sorted :   1.047059   0.001725   1.048784 (  1.050457)
search_node_bsearch:   0.388979   0.000700   0.389679 (  0.390126)
>> right query -------------------------------------------------------
search_node        :   0.761358   0.001011   0.762369 (  0.763245)
search_node_sorted :   1.005419   0.001159   1.006578 (  1.007741)
search_node_bsearch:   0.581907   0.001079   0.582986 (  0.583866)
>>> Intervals: 6 ***************************************************************
                           user     system      total        real
>> left query --------------------------------------------------------
search_node        :   0.791082   0.000830   0.791912 (  0.793027)
search_node_sorted :   1.134816   0.001724   1.136540 (  1.137947)
search_node_bsearch:   0.569002   0.000991   0.569993 (  0.571081)
>> centered query ----------------------------------------------------
search_node        :   0.891427   0.001421   0.892848 (  0.894489)
search_node_sorted :   1.231044   0.002040   1.233084 (  1.234685)
search_node_bsearch:   0.390488   0.000711   0.391199 (  0.391803)
>> right query -------------------------------------------------------
search_node        :   0.855732   0.001264   0.856996 (  0.858243)
search_node_sorted :   1.206386   0.001371   1.207757 (  1.208807)
search_node_bsearch:   0.587420   0.000684   0.588104 (  0.588948)
>>> Intervals: 7 ***************************************************************
                           user     system      total        real
>> left query --------------------------------------------------------
search_node        :   0.938393   0.001617   0.940010 (  0.941799)
search_node_sorted :   1.414355   0.001930   1.416285 (  1.418304)
search_node_bsearch:   0.573533   0.000826   0.574359 (  0.575299)
>> centered query ----------------------------------------------------
search_node        :   0.967661   0.001419   0.969080 (  0.970986)
search_node_sorted :   1.425753   0.002027   1.427780 (  1.429500)
search_node_bsearch:   0.384979   0.000723   0.385702 (  0.386714)
>> right query -------------------------------------------------------
search_node        :   0.985602   0.001275   0.986877 (  0.988527)
search_node_sorted :   1.423696   0.001781   1.425477 (  1.427529)
search_node_bsearch:   0.667344   0.000931   0.668275 (  0.669350)
>>> Intervals: 8 ***************************************************************
                           user     system      total        real
>> left query --------------------------------------------------------
search_node        :   1.096039   0.001490   1.097529 (  1.099284)
search_node_sorted :   1.629541   0.002240   1.631781 (  1.634775)
search_node_bsearch:   0.530544   0.000905   0.531449 (  0.532137)
>> centered query ----------------------------------------------------
search_node        :   1.091209   0.001074   1.092283 (  1.093627)
search_node_sorted :   1.602735   0.002120   1.604855 (  1.606814)
search_node_bsearch:   0.393407   0.000901   0.394308 (  0.395315)
>> right query -------------------------------------------------------
search_node        :   1.121127   0.001515   1.122642 (  1.124060)
search_node_sorted :   1.622824   0.002044   1.624868 (  1.626561)
search_node_bsearch:   0.671208   0.001137   0.672345 (  0.673149)
>>> Intervals: 9 ***************************************************************
                           user     system      total        real
>> left query --------------------------------------------------------
search_node        :   1.123871   0.001629   1.125500 (  1.127536)
search_node_sorted :   1.617628   0.001970   1.619598 (  1.622036)
search_node_bsearch:   0.565249   0.000585   0.565834 (  0.566251)
>> centered query ----------------------------------------------------
search_node        :   1.204953   0.001744   1.206697 (  1.208608)
search_node_sorted :   1.778111   0.002461   1.780572 (  1.782756)
search_node_bsearch:   0.423217   0.000658   0.423875 (  0.424780)
>> right query -------------------------------------------------------
search_node        :   1.275738   0.001736   1.277474 (  1.279188)
search_node_sorted :   1.784942   0.002243   1.787185 (  1.788962)
search_node_bsearch:   0.835533   0.001431   0.836964 (  0.839916)
>>> Intervals: 10 **************************************************************
                           user     system      total        real
>> left query --------------------------------------------------------
search_node        :   1.294349   0.001877   1.296226 (  1.297781)
search_node_sorted :   1.792679   0.002953   1.795632 (  1.801279)
search_node_bsearch:   0.576735   0.000672   0.577407 (  0.577920)
>> centered query ----------------------------------------------------
search_node        :   1.368983   0.002562   1.371545 (  1.377482)
search_node_sorted :   1.930898   0.002259   1.933157 (  1.934894)
search_node_bsearch:   0.392094   0.000681   0.392775 (  0.393237)
>> right query -------------------------------------------------------
search_node        :   1.346151   0.001161   1.347312 (  1.348517)
search_node_sorted :   1.927809   0.002533   1.930342 (  1.933438)
search_node_bsearch:   0.760859   0.001203   0.762062 (  0.763728)
Benchmark IPS
>>> Intervals: 1 ***************************************************************
Warming up --------------------------------------
search_node        :   105.392k i/100ms
search_node_sorted :   101.250k i/100ms
search_node_bsearch:    93.668k i/100ms
Calculating -------------------------------------
search_node        :      1.023M (± 4.7%) i/s -      5.164M in   5.057773s
search_node_sorted :    998.401k (± 2.1%) i/s -      5.062M in   5.072925s
search_node_bsearch:    917.404k (± 2.1%) i/s -      4.590M in   5.005239s
>>> Intervals: 2 ***************************************************************
Warming up --------------------------------------
search_node        :    91.874k i/100ms
search_node_sorted :    84.642k i/100ms
search_node_bsearch:    91.686k i/100ms
Calculating -------------------------------------
search_node        :    902.519k (± 5.0%) i/s -      4.502M in   5.002602s
search_node_sorted :    831.617k (± 3.6%) i/s -      4.232M in   5.095898s
search_node_bsearch:    882.119k (± 3.9%) i/s -      4.493M in   5.101410s
>>> Intervals: 3 ***************************************************************
Warming up --------------------------------------
search_node        :    82.912k i/100ms
search_node_sorted :    71.660k i/100ms
search_node_bsearch:    82.199k i/100ms
Calculating -------------------------------------
search_node        :    818.151k (± 2.8%) i/s -      4.146M in   5.071190s
search_node_sorted :    739.639k (± 3.2%) i/s -      3.726M in   5.043409s
search_node_bsearch:    859.761k (± 3.2%) i/s -      4.357M in   5.072533s
>>> Intervals: 4 ***************************************************************
Warming up --------------------------------------
search_node        :    76.066k i/100ms
search_node_sorted :    66.578k i/100ms
search_node_bsearch:    86.782k i/100ms
Calculating -------------------------------------
search_node        :    751.632k (± 2.3%) i/s -      3.803M in   5.062856s
search_node_sorted :    660.350k (± 4.7%) i/s -      3.329M in   5.054221s
search_node_bsearch:    865.556k (± 2.5%) i/s -      4.339M in   5.016253s
>>> Intervals: 5 ***************************************************************
Warming up --------------------------------------
search_node        :    68.025k i/100ms
search_node_sorted :    59.840k i/100ms
search_node_bsearch:    86.743k i/100ms
Calculating -------------------------------------
search_node        :    685.046k (± 2.5%) i/s -      3.469M in   5.067622s
search_node_sorted :    589.422k (± 4.1%) i/s -      2.992M in   5.086056s
search_node_bsearch:    856.151k (± 4.0%) i/s -      4.337M in   5.074753s
>>> Intervals: 6 ***************************************************************
Warming up --------------------------------------
search_node        :    65.648k i/100ms
search_node_sorted :    50.798k i/100ms
search_node_bsearch:    86.379k i/100ms
Calculating -------------------------------------
search_node        :    636.683k (± 2.7%) i/s -      3.217M in   5.056001s
search_node_sorted :    527.625k (± 4.5%) i/s -      2.641M in   5.017275s
search_node_bsearch:    836.447k (± 3.7%) i/s -      4.233M in   5.067301s
>>> Intervals: 7 ***************************************************************
Warming up --------------------------------------
search_node        :    58.337k i/100ms
search_node_sorted :    49.168k i/100ms
search_node_bsearch:    84.843k i/100ms
Calculating -------------------------------------
search_node        :    596.302k (± 2.8%) i/s -      3.034M in   5.091405s
search_node_sorted :    496.595k (± 3.8%) i/s -      2.508M in   5.057162s
search_node_bsearch:    814.451k (± 4.8%) i/s -      4.072M in   5.012782s
>>> Intervals: 8 ***************************************************************
Warming up --------------------------------------
search_node        :    54.488k i/100ms
search_node_sorted :    46.428k i/100ms
search_node_bsearch:    83.828k i/100ms
Calculating -------------------------------------
search_node        :    545.984k (± 3.7%) i/s -      2.779M in   5.096794s
search_node_sorted :    458.736k (± 4.0%) i/s -      2.321M in   5.068991s
search_node_bsearch:    816.489k (± 3.4%) i/s -      4.108M in   5.036792s
>>> Intervals: 9 ***************************************************************
Warming up --------------------------------------
search_node        :    50.337k i/100ms
search_node_sorted :    43.065k i/100ms
search_node_bsearch:    82.861k i/100ms
Calculating -------------------------------------
search_node        :    500.379k (± 4.0%) i/s -      2.517M in   5.038098s
search_node_sorted :    418.650k (± 3.8%) i/s -      2.110M in   5.048243s
search_node_bsearch:    803.520k (± 3.6%) i/s -      4.060M in   5.059793s
>>> Intervals: 10 **************************************************************
Warming up --------------------------------------
search_node        :    47.949k i/100ms
search_node_sorted :    40.537k i/100ms
search_node_bsearch:    83.422k i/100ms
Calculating -------------------------------------
search_node        :    478.889k (± 3.8%) i/s -      2.397M in   5.013619s
search_node_sorted :    396.956k (± 3.8%) i/s -      1.986M in   5.011312s
search_node_bsearch:    803.695k (± 3.8%) i/s -      4.088M in   5.093751s

Analysis

There are a few main points I'd like to highlight.

  1. search_node_sorted is slower than search_node for all interval counts tested. Because of this I think that this PR should not include this implementation. As to why it's slower I think it's because each is slow, but I'm not sure.
  2. search_node_bsearch is faster on average when s_center has 3 intervals or more (and faster for all query types at 4 intervals or more). I would have a discussion and see what people think about this option and whether we should bring it in.
  3. These are certainly not the only algorithms (nor the only ways to implement them), so improvements should be suggested

Benchmark Script & Code: https://gist.github.com/amarzot-yesware/dc8aea0e9970e92c86cb35389254c8a9
Branch on my fork: https://github.com/amarzot-yesware/interval-tree/tree/faster_node_search

@amarzot-yesware
Copy link

I was just re-reading the interval tree wikipedia page and it describes exactly the approach I implemented in my branch.

@danielpetri1
Copy link
Author

danielpetri1 commented Jul 15, 2021

Hi @amarzot-yesware, thanks for your in-depth benchmarking and explanation.
I would imagine that in most applications, having 4 or more intervals is the norm, so I believe it would be reasonable to bring in your approach instead of the current one.

The approach from this PR should be faster for point searches.

@amarzot-yesware
Copy link

I would imagine that in most applications, having 4 or more intervals is the norm,

The trick is having 4 or more intervals per node, but maybe that it what you meant, and maybe that too is the norm (i'd imagine it is).

The approach from this PR should be faster for point searches.

As you recommended on my gist, I'll do some benchmarking to confirm and post results.

@danielpetri1
Copy link
Author

As you recommended on my gist, I'll do some benchmarking to confirm and post results.

Thank you, much appreciated.

As requested, linting changes are now separate from functionality changes to make reviewing easier.
The sorted parameter was removed since I believe it was redundantly sorting the output array, which
is probably why it was slower than the current implementation.
@danielpetri1
Copy link
Author

danielpetri1 commented Sep 16, 2021

The requested changes and feedback were taken into account in the last push.

The unique parameter was left as true to conform with the current implementation, but we should consider leaving it off per default since uniq is pretty slow.

Linting changes with Rubocop were kept separate in another commit, as well as the removal of duplicate method calls that Reek was complaining about. Personally I think it affects readability negatively, but it may be worth it in case it makes the code more efficient. I also checked the code with Fasterer and it did not report the use of each over for as an issue.

The previous benchmarks did not take point searches in account. It would be interesting to see a comparison between the approaches in this regard, the impact of uniq/disabing sorting, and the last few changes. (I tried running the benchmarking code but haven't gotten it to work yet)

@danielpetri1 danielpetri1 deleted the patch-2 branch October 9, 2021 12:30
@danielpetri1 danielpetri1 restored the patch-2 branch October 9, 2021 13:48
@danielpetri1 danielpetri1 reopened this Oct 9, 2021
@danielpetri1
Copy link
Author

Hi,
the latest commit improves the speed of stabbing queries by further 55% by taking the maximum interval end within the node into account. In total, the the speed gains amount to 70%. Below are the benchmarking results:

Benchmarks
Generating 10000 intervals starting in range [0...100000] Generating 100000 queries

Recursive implementation with one list per node, using average (s_center not sorted)
Total elapsed time: 5.6156

Recursive implementation with one list per node, using median (s_center not sorted)
Total elapsed time: 5.8209

Iterative implementation with one list per node using average (s_center sorted), not taking maximum interval end into account [Previous version]
Total elapsed time: 2.6065

Iterative implementation with one list per node using average (s_center sorted), taking maximum interval end into account [Pushed version]
Total elapsed time: 1.1675

Iterative implementation with one list per node using median (s_center sorted), taking maximum interval end into account
Total elapsed time: 1.1841

Iterative implementation with two lists per node, using average (s_center sorted)
Total elapsed time: 1.6631

Iterative implementation with two lists per node, using median (s_center sorted)
Total elapsed time: 1.6696

Iterative implementation with two lists per node, using average (s_center sorted), taking maximum interval end into account
Total elapsed time: 7.0531

Using average (s_center sorted), taking maximum interval end into account, no rationals
Total elapsed time: 1.0983

Using median (s_center sorted), taking maximum interval end into account, no rationals
Total elapsed time: 1.1264

Code is available here.

I've played around with centering the intervals using the median of the values instead of the mean but this wasn't faster, even though it should result in more balanced trees. Storing two lists per node was also not quicker.

By not casting to rationals the performance improves a bit, but then the time ranges stop working.

Any updates on #13 being merged?

@maddymarkovitz
Copy link
Contributor

@danielpetri1 wrote:

Any updates on #13 being merged?

Unfortunately we (the GreenSync team) are finding that we do not have the resources to actively maintain this gem. We are also no longer using it ourselves which makes it difficult to make decisions about what direction to take development in.
With that in mind, we are looking for a new maintainer. Seeing as you've been active in making PRs against interval-tree recently, is this a role you would consider taking on?

@danielpetri1
Copy link
Author

I'd be interested, yes, although @amarzot-yesware seems to have quite a few good ideas here besides being quite experienced. If they're not interested I could give it a shot.

@maddymarkovitz
Copy link
Contributor

Good call. @amarzot-yesware what do you think? Are you interested in becoming a maintainer (or co-maintainer?)

@amarzot-yesware
Copy link

@maddymarkovitz @danielpetri1 I appreciate the offer! I would certainly be interested, but I'm not sure I have the bandwidth to be a full time maintainer. If @danielpetri1 would be up for it, I would join them as a co-maintainer. Another thing to note is that this is my work account. If I were to be a maintainer, I'd prefer to use my personal account @amarzot

@jeremiahrose
Copy link

@danielpetri1 @amarzot-yesware we're still looking for maintainers if you guys are interested :)

@amarzot
Copy link

amarzot commented Dec 19, 2022

@jeremiahrose Sorry, I don't think it's a good idea for me, as I no longer work with this tech (or at the company that used it). Best of luck!

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.

6 participants