Efficient AVL Tree in C#
bitlush.com
bitlush.com
Now the interesting thing is that rebalancing can also be done using a lesser-known top-down algorithm, allowing for a non-recursive algorithm that doesn't rely on parent pointers.
https://web.archive.org/web/20180315093634/http://neil.brown... https://web.archive.org/web/20180320115617/http://neil.brown...
The downside of such an approach of using two successive top-down algorithms is that a naïve implementation would require twice as many object comparisons. Opposed to traversing a tree bottom-up, you now have to make a choice. As both traversals (insertion/deletion and rebalancing) are identical, this can easily be prevented by storing the path during the first traversal. As AVL trees are strongly balanced and only a finite number of nodes fit in an address space, storing a path only takes very little space (a bitmask would use less than two uintptr_t's).
I've implemented such an algorithm for FreeBSD's copy of the POSIX tsearch() and tdelete() functions a couple of years ago:
https://svnweb.freebsd.org/base/head/lib/libc/stdlib/tsearch... https://svnweb.freebsd.org/base/head/lib/libc/stdlib/tdelete...
It made a lot of sense in this case to not have a parent pointer in the tree nodes, for the reason that that would bump the size of a node just over 32 bytes (left, right, parent and data pointers, and a balance count). This would cause libc's allocator to allocate 64 bytes per node instead.
I also seem to remember that the performance of this implementation was pretty much on par with a conventional recursive implementation. The only differences were that use of stack space was a lot less (i.e., constant) and that the machine code generated by Clang was a tiny bit smaller. I suspect that's because the optimizer didn't have to deal with recursive code.
A simple benchmark would've gone a long way here.
Edit - also this:
if (_comparer.Compare(key, node.Key) < 0)
{
node = node.Left;
}
else if (_comparer.Compare(key, node.Key) > 0)
{
node = node.Right;
}
needlessly doubles the number of Compare() calls in Delete() and Search(). Insert() however does it right.I enjoyed the article and applaud the author's work, but I don't feel good about the claim to beat the standard library without discussing the benchmark that indicates this. Insertion pattern matters a lot for this kind of thing.
See also the 'masstree' which has a similar premise - https://news.ycombinator.com/item?id=18199086
I may have missed it, but I'd love to know the versions of .net framework this was performed against. I saw a really interesting post in MSDN about how they've been working on performance improvements for underlying data structures. I would love to see this up against dotnet core 2.1 or >. For reference, this was the post:
https://blogs.msdn.microsoft.com/dotnet/2018/04/18/performan...
Eg, if they test this against SortedDictionary in cotnet core, they should also implement the mentioned algorithm in dotnet core.
I think what they mean is, their algorithm is faster than the red-black tree algorithm.
https://blogs.msdn.microsoft.com/dotnet/2017/06/07/performan...
"The non-recursive way is more efficient as the CLR does not have to keep pushing and popping its call stack (which is quite slow). The non-recursive way is unfortunately harder to code. So the challenge was on!"
* I have a different take on writing "efficient" AVL trees. Optimizing for human comprehension, I have an implementation strictly under 100 lines of code: https://www.nayuki.io/res/aa-tree-set/BasicAvlTreeSet.java . (Unlike bitlush, mine uses recursion, explicit height tracking, and no parent pointers.)
* Java's TreeMap<K,V> is similar to C#'s SortedDictionary<TKey, TValue>. Java also uses a red-black tree.
* bitlush claims a 2× to 4× speed-up over the standard library's SortedDictionary. Red-black trees are asymptotically optimal (O(log n)), so they can't improve by more than a constant factor. I get the vague sense that bitlush was able to make a constant speed-up by simplifying some unused code or offering less functionality than the standard library. I doubt that the standard library is being intentionally slow.
* If they wanted a faster sorted dictionary, they should use a B-tree. Rust does this, and it is much more cache-friendly. For example, see http://dtrace.org/blogs/bmc/2018/09/28/the-relative-performa... .
* I think having parent pointers is a more expensive tradeoff than recursion. A binary tree node must have 2 child pointers, and adding a parent pointer brings the total to 3; this costs O(n) space. This cost is borne by all nodes all the time. Meanwhile, using recursion for insert/delete/iterator costs O(log n) space, and only temporarily during the operation.
* The author claims "I also wrote a lot of tests to ensure the code is rock solid.". The test suite is at https://github.com/bitlush/avl-tree-c-sharp/tree/master/Bitl... . The suite looks a bit short to me, and I don't see more than ten elements be tested. I would suggest the author implement one of my favorite ways of testing data structures: Performing thousands of random operations on your data structure, and comparing the results against a naive or standard library implementation.
So how das it compare to Dictionary<TKey, TValue>? Given that operations on a balanced search tree are O(log(n)) while they are O(1) amortized - and O(n) worst case - on a hash-based dictionary, it doesn't seems too plausible that even a fast implementation could score a win. Not to mention the much worse locality of a tree. In some rare scenarios having O(n) worst case performance might be an issue but unless you actually need an ordered dictionary I have a hard time believing that a tree-based implementation can outperform a hash-based implementation.
I also worked (as a mental challenge) on an interval set using a balanced tree. I was less succesful in keeping the tree balanced in this case. See: http://www.iwriteiam.nl/D1808.html#4
2) How is it efficient? compared to what exactly?