experimenting with splay trees

Monkey Forums/Monkey Programming/experimenting with splay trees

Nobuyuki(Posted 2015) [#1]
Heya fellas,

In preparation for a fork of angelfont which has better support for unicode (amongst other things), I decided to port a splay tree implementation over to Monkey. I chose this one by Josh Israel, since it was easy for me to read and didn't require a lot of extra work to make it "match" the canonical style Mark used for Maps.

For those who aren't familiar with splay trees, they're a form of binary search tree which is self-optimizing -- fetches from the tree perform a 'splay' operation which over time puts your most frequently-accessed elements closer to the root so that there is less cycles used to fetch those elements over time. For certain operations (such as, for example, fetching char information from a potentially very large unicode charset where at runtime you'll usually only be accessing a specific subset, but accessing it frequently), this type of tree may provide significant performance benefits.

However, in practice, the implementation I went with seems much less than optimal. The number of splay operations done on a large tree seems to give my computer a big headache on large but reasonably-sized trees. Particularly, at around a tree size of 5900 elements, this code ends up blowing the call stack on both desktop and html5 targets due to too much recursion.

https://gist.github.com/nobuyukinyuu/6555bf12d3da849d68cb

I never took any cs or programming courses in school, so I'm afraid that binary search trees are something I don't have a really good grasp on. What are the best practices here? How many things in this implementation are suboptimal in addition to the recursion? etc. Any comments would be appreciated :)


muddy_shoes(Posted 2015) [#2]
Hi,

The problem with splay trees is that the situations where they truly provide an advantage aren't as common as the description suggests. In particular if your dataset is static and your comparison function is cheap then it's unlikely that a splay tree will be able to beat a standard balanced tree implementation like Monkey's standard map. The reason is that splaying has a cost that you're paying per access and if comparisons are cheap then that cost is way more than the few comparisons you save from having items nearer the top of the tree. You can offset this effect by only splaying if a fetch goes deep or if you fetch the same thing twice in a row etc. but it all gets very situation specific.

So, if your intended usage involves an integer key and a fixed dataset for font image lookups then I doubt splay trees will help you. If your comparison function is more expensive and/or your dataset is volatile (like a cache) then they might be worth trying out.

Anyway, I hacked in some iterative code to fix your implementations call-stack issue and had a play to verify my thinking. The code is below if it's of any use to you.




Nobuyuki(Posted 2015) [#3]
hey, thanks a lot man :)

It's all very interesting stuff. I was hoping that someone could get it to work iteratively, and I agree that the splay cost probably outweighs the traversal cost if it's done on every operation. The idea originally was if I could wrap my head around it, to modify / extend the class to basically do what you've done and only splay if the traversal height goes above a certain level, since only a few characters are usually used in a character set at one time, and they're fetched frequently by existing AngelFont code (this is okay in the existing version, because it only supports ascii and all chars are in an array).

That being said, the character map for an international font set, even one that supports CJK, might not exceed 12000 characters, so I'm wondering if there'd be any gain at all if the splay operation stops at a certain point? Traversing >5 levels deep consistently over time might be less efficient than monkey's Map (red-black tree). I guess I just wouldn't know without some real world tests.

I had concerns about efficiency when adapting AngelFont to support unicode for this project, which is why I added a few "ifs" in there to handle ascii the same way as the original, but with all extended characters using a Map. At the time I did it, I was under the belief that the lookup cost for a decent-sized international charset would eat a lot into a game's frame budget, and it was something that kept me from incorporating that support into angelfont-tryouts.

Would you mind me taking the work you've done and putting it up on GitHub? I'll probably add/change a few more things to make it more in sync with Map's methods, maybe throw in some ObjectEnumerators or something.


muddy_shoes(Posted 2015) [#4]
It's easy enough to test your scenario with the code above. Plug in 12000 elements, a reasonable number for the total characters accesses by an app (<100 for English, certainly) and crank the repeats to a few thousand so the timing has something to work with. For me on HTML5 the splay tree performance is 6x slower than the map even with thresholding turned on.

If you really felt the need to optimise lookups then I'd suggest either generating a secondary cache map or just some sort of lookup table. However, and I know this tends to annoy people when I say this in performance threads, it probably will make no noticeable difference, especially when talking about text where the rendering is generally far more expensive than any lookup will be.

As for the code, use it however you like. It's mostly just a conversion of the wikipedia sample code and some trivial bits from my util libs.


Nobuyuki(Posted 2015) [#5]
Results are what's important, I don't think I have to worry about lookup times when using a Map most likely :)

probably gonna have to figure out how to do a Remove non-recursively, since I apparently never even tested the code (it wouldn't even compile anyway since I missed a declare line when porting, whoops!)


muddy_shoes(Posted 2015) [#6]
Here's a version with a delete. I've also manufactured a scenario where the splay tree does eventually beat out the map. It's not your scenario unfortunately.




Nobuyuki(Posted 2015) [#7]
haha, doin' all the work for me, huh?

The second example is kinda weird. I'm starting to trim and rename functions since it's becoming a bit of a mess. The old recursive Splay() isn't used anymore, so that's the first to go. I've also renamed Insert() to Set(), changed its output type, moved several methods to Private, and renamed Delete() to Remove() while deleting the old Remove().

In your second example, you're doing an insert/remove cycle, but it should be noted that in the code provided, it appears that SplayTree.Insert() as it was would never splay because Splay() was never called anywhere in the method(). This has been fixed. Furthermore, Map.Insert() is depreciated and is just a call to Set(), so .... :)

Here's the code so far after an initial cleanup: https://github.com/nobuyukinyuu/monkey-splaytree


muddy_shoes(Posted 2015) [#8]
Sure, I was just hacking around so I left old code lying about. The not splaying on insert is intentional, although I must have pasted older code as I changed Set to have a splay option. It only makes sense to splay on insert if you expect retrievals of recently inserted elements.


Nobuyuki(Posted 2015) [#9]
by the way, do you know if there are "canonical" best practices for ObjectEnumerators? I personally don't care if the tree gets traversed in any particular order, but it seems that the existing containers spit out objects in a sorted order, whereas traversing the splay tree in a similar fashion to Find() would seem likely to spit out elements in semi-last-accessed order. I'm not aware that anyone should expect EachIn to return the values from any container in any particular order; was just asking if you know if this is just something most people assume that most containers will do. Obviously expecting that seems incompatible with the way a splay tree is structured.


muddy_shoes(Posted 2015) [#10]
Nope. People make all sorts of assumptions about how collections should be enumerated. I tend to think that the default should be whatever is cheapest unless the collection makes explicit claims about being sorted. I tend to write in order enumerators for ordered trees though.


Nobuyuki(Posted 2015) [#11]
Cool. Allright, I've updated the source code with enumerators. Using EachIn on the tree itself produces a Stack<Node<K,V>> for the duration of the traversal. In addition, the usual enumerator overhead applies, including extra objects created to allow eachIn to work on .Keys() and .Values().