Skip to content

[CompilerPerf] Changed Map.count + Set.count from O(n) to O(1) (AVL logic based on size not height) - #5365

Closed
manofstick wants to merge 3 commits into
dotnet:masterfrom
manofstick:map_faster_size
Closed

[CompilerPerf] Changed Map.count + Set.count from O(n) to O(1) (AVL logic based on size not height)#5365
manofstick wants to merge 3 commits into
dotnet:masterfrom
manofstick:map_faster_size

Conversation

@manofstick

Copy link
Copy Markdown
Contributor

Splitting #5360.

Currently Map stores the height of each node, and uses that to determine if it should rebalance itself. This change stores the count of values instead of the height - and rebalances a node when one side is over twice the size of the other - I believe algorithmically equivalent - although in practice it may slightly change performance, as for particular cases a better/worse tree could be in an inner loop. Intuitively I think this should build better trees with more information, but I haven't attempted to mathematically prove this.

This makes Map.count an O(1) operation rather than an O(n) operation.

@manofstick

Copy link
Copy Markdown
ContributorAuthor

This gist was about the same:

https://gist.github.com/manofstick/11b5ac3c3cf993ce32e69d04a6549161

This gist was slightly better - but less than 5% across the board (but possibly performance improvement is being masked by the lack of #5307 - as much more time than necessary will be lost in IComparer.Compare)

https://gist.github.com/manofstick/275fe8ed62091aec52cd382548719f2a

And this gist was consistently better with the new balancing

https://gist.github.com/manofstick/e97dc9775bf01fd22b2f238cac9f1c27

testbittagepercent
construct64-bit88%
construct32-bit81%
access64-bit89%
access32-bit81%

@forki

Copy link
Copy Markdown
Contributor

can you please apply the same to set.fs and TaggedCollections.fs (2 times)

let empty = MapEmpty

let height = function
let size = function

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

please inline size function

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

No really keen to inline whilst MapOne exists. size as it stands has to do two attempted types casts which easily outweight a function call.

@manofstick

Copy link
Copy Markdown
ContributorAuthor

Gist for quick Set check: https://gist.github.com/manofstick/b25efcf69fb7791357918d5a780ac438

Seems to be ~10% faster in this gist... (create + union + element check) . But haven't done exhaustive testings...

test typebittagetest sets size%
sequential32-bit0103%
sequential32-bit2585%
sequential32-bit5092%
sequential32-bit7585%
sequential32-bit10091%
sequential32-bit12590%
sequential32-bit15092%
sequential32-bit17592%
sequential32-bit20093%
sequential32-bit22591%
sequential32-bit25092%
random32-bit0100%
random32-bit2583%
random32-bit5088%
random32-bit7586%
random32-bit10089%
random32-bit12588%
random32-bit15087%
random32-bit17588%
random32-bit20089%
random32-bit22590%
random32-bit25089%
sequential64-bit0100%
sequential64-bit2588%
sequential64-bit5089%
sequential64-bit7588%
sequential64-bit10091%
sequential64-bit12589%
sequential64-bit15090%
sequential64-bit17589%
sequential64-bit20089%
sequential64-bit22589%
sequential64-bit25090%
random64-bit0102%
random64-bit2595%
random64-bit50100%
random64-bit7593%
random64-bit10097%
random64-bit12593%
random64-bit15093%
random64-bit17594%
random64-bit20092%
random64-bit22593%
random64-bit25093%

@manofstickmanofstick changed the title Changed Map.count from O(n) to O(1)[CompilerPerf] Changed Map.count + Set.count from O(n) to O(1) (AVL logic based on size not height)Jul 23, 2018
@manofstick

Copy link
Copy Markdown
ContributorAuthor

@forki

You can see in TaggedCollections.fs that the optimization to remove the single node has already been done (at least I assume #if ONE is not set anywhere for the build...) I'll push my modifications up as soon as I get a green build from Set.fs... (unless I fall asleep, which is possible :-)

@forki

Copy link
Copy Markdown
Contributor

yes setone and mapone can probably go as well. but please do in separate PR. it will keep things easier for VF# team to accept

@TIHanTIHan added Tenet-Performance Area-Library Issues for FSharp.Core not covered elsewhere labels Jul 23, 2018
@manofstick

Copy link
Copy Markdown
ContributorAuthor

@forki yes, yes, I just meant I would push the TaggedCollections.fs changes after green... (and after sleep!)

@cartermp

Copy link
Copy Markdown
Contributor

@manofstick Just curious about this statement:

This change stores the count of values instead of the height - and rebalances a node when one side is over twice the size of the other - I believe algorithmically equivalent - although in practice it may slightly change performance, as for particular cases a better/worse tree could be in an inner loop.

I don't think this is true, as the current implementation re-balances once the height of one sub-tree is 2 higher than the other. This implementation re-balances after it's twice as high. I'm not familiar with the performance of AVL trees beyond what I learned in college, so I don't know what the long-term ramifications of this would be. But I'm certainly not opposed to basing count off of size and the initial performance results 😄

@manofstick

manofstick commented Jul 24, 2018

Copy link
Copy Markdown
ContributorAuthor

@cartemp

Yes, I don't mean the same trees. That was stated. I was probably a bit strong without the word equivalent, but I was meaning computational complexity. Still getting balanced binary trees that are still created using the same AVL transforms. But yes the rest is a bit hand wavey! (I've actually sent it to a mate at University of British Columbia to do an analysis of, but he's usually pretty busy. But we'll see... At the moment I'm trusting intuition and tests)

@forki

forki commented Jul 25, 2018 via email

Copy link
Copy Markdown
Contributor

@manofstick

Copy link
Copy Markdown
ContributorAuthor

@forki

It's possible. I haven't run the numbers. Anyway am seeing a non-insignificant performance improvement for "smallish" (thousands) of node trees - so it's possible that it's just doing better rebalancing. Anyway, this is why I'm running random and sequential data in tests in case there are degenerate sequences.

...nd I'm willing to accept that in 1962 when AVL trees were first described, they were more interested in saving memory, and so the cost of carrying the size with each node would of been decadent. But considering we were already carrying a int height - well I think it's re-purpose was OK.

Anyway, this branch only has this change in it, so it can be tested in isolation...

@manofstick

Copy link
Copy Markdown
ContributorAuthor

Another test: https://gist.github.com/manofstick/f285aa7b16025aabd4f60a4b8413ab81

bittageayende #%
32-bit25064.00%
32-bit50066.15%
32-bit75077.73%
32-bit100076.56%
32-bit125077.52%
32-bit150077.93%
64-bit25054.39%
64-bit50069.40%
64-bit75084.39%
64-bit100084.53%
64-bit125084.18%
64-bit150083.75%

@forki

Copy link
Copy Markdown
Contributor

what about very large size? are we now restricting the count since we track the size and not height as an int?

@manofstick

Copy link
Copy Markdown
ContributorAuthor

@forki

The first gist mentioned in #5365 (comment) deals with large n, where it seems to return to about the same performance.

@manofstick

Copy link
Copy Markdown
ContributorAuthor

@forki - but I'll create some more tests over the days ahead...

@forki

Copy link
Copy Markdown
Contributor

didn't mean perf - I meant are getting issues with the count of elements when the size integer overflows? In theory the height overflows much later.

@manofstick

Copy link
Copy Markdown
ContributorAuthor

@forki - yes. Could be a showstopper?

@forki

Copy link
Copy Markdown
Contributor

dunno. Not even sure if it is a real problem or just imaginary

@zpodlovics

Copy link
Copy Markdown

This data structure looks like a Weight-balanced tree.

@forki It seems that the other Weight-balanced tree implementations solved it by using the logarithmic of the size:

"In order to ensure performance, the algorithm keeps the height of a tree
logarithmic to its size by balancing the sizes of the subtrees in each node." [1] [2]

How the balancing implemented?

"A weight-balanced tree (WBT) is a binary search tree, whose balance is based on the sizes
of the subtrees in each node. Although purely functional implementations on a variant
WBT algorithm are widely used in functional programming languages, many existing
implementations do not maintain balance after deletion in some cases.
The difficulty lies
in choosing a valid pair of rotation parameters: one for standard balance and the other for
choosing single or double rotation. This paper identifies the exact valid range of the rotation
parameters for insertion and deletion in the original WBT algorithm where one and only
one integer solution exists.
Soundness of the range is proved using a proof assistant Coq.
Completeness is proved using effective algorithms generating counterexample trees. For two
specific parameter pairs, we also proved in Coq that set operations also maintain balance.
Since the difference between the original WBT and the variant WBT is small, it is easy to
change the existing buggy implementations based on the variant WBT to the certified original
WBT with a rational solution." [1] [2]

[1] https://yoichihirai.com/bst.pdf
[2] http://www.mew.org/~kazu/proj/weight-balanced-tree/

@manofstick

Copy link
Copy Markdown
ContributorAuthor

@KevinRansom

Closing this. Was always kind of a side thing. Better to follow the path to #5463

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area-LibraryIssues for FSharp.Core not covered elsewhere

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@manofstick@forki@cartermp@zpodlovics@TIHan