Page MenuHomeFreeBSD

Let tsearch()/tdelete() use an AVL tree.
ClosedPublic

Authored by ed on Dec 6 2015, 12:07 PM.
Tags
None
Referenced Files
Unknown Object (File)
Mar 11 2024, 5:48 AM
Unknown Object (File)
Mar 11 2024, 5:48 AM
Unknown Object (File)
Mar 11 2024, 5:48 AM
Unknown Object (File)
Mar 11 2024, 5:48 AM
Unknown Object (File)
Mar 11 2024, 5:48 AM
Unknown Object (File)
Mar 11 2024, 5:39 AM
Unknown Object (File)
Mar 11 2024, 5:39 AM
Unknown Object (File)
Mar 11 2024, 5:39 AM
Subscribers

Details

Summary

The existing implementations of POSIX tsearch() and tdelete() don't
attempt to perform any balancing at all. Testing reveals that inserting
100k nodes into a tree sequentially takes approximately one minute on my
system.

Though most other BSDs also don't use any balanced tree internally, C
libraries like glibc and musl do provide better implementations. glibc
uses a red-black tree and musl uses an AVL tree.

Red-black trees have the advantage over AVL trees that they only require
O(1) rotations after insertion and deletion, but have the disadvantage
that the tree has a maximum depth of 2*log2(n) instead of 1.44*log2(n).
Still, my take is that it's better to focus on having a lower maximum
depth, for the reason that in the case of tsearch(), the invocation of
the comparator likely dominates the running time.

This change replaces the tsearch() and tdelete() functions by versions
that create an AVL tree. Compared to musl's implementation, this
version is optimized in two different ways:

  • We don't keep track of heights; just balances. This is sufficient. This has the advantage that it reduces the number of nodes that are being accessed.
  • Don't use any recursion at all. We know that the tree cannot 2^64 elements in size, so the height of the tree can never be larger than 96. Use a 128-bit bitmask to keep track of the path that is computed. This allows us to iterate over the same path twice, meaning we can apply rotations from top to bottom.

Inserting 100k nodes into a tree now only takes 0.015 seconds. This implementation is about 1/3 faster than glibc, and unlike glibc, it uses a fixed amount of memory.

Test Plan

This implementation already passes CloudABI's test suite, as it is part
of CloudABI's C library. I've also extracted the tests and run them on
FreeBSD natively, using the patched up C library.

Diff Detail

Repository
rS FreeBSD src repository - subversion
Lint
Lint Not Applicable
Unit
Tests Not Applicable

Event Timeline

ed retitled this revision from to Let tsearch()/tdelete() use an AVL tree..
ed updated this object.
ed edited the test plan for this revision. (Show Details)
ed added reviewers: theraven, jilles.
ed edited edge metadata.

For some reason, Arcanist creates empty files?

ed added a reviewer: emaste.

Improve man page, as suggested by emaste

ed edited edge metadata.

Port CloudABI's tests over to FreeBSD, so that we can automatically validate our implementation.

Proposed by: emaste

ed edited edge metadata.
  • Revert faulty change to the tests Makefile.
  • Add missing $FreeBSD$ tags.
  • Simplify tdelete(): there is no need to test both branches.
ed edited edge metadata.
ed removed rS FreeBSD src repository - subversion as the repository for this revision.

Allow rootp to be NULL. This is kind of weird, but is allowed by POSIX.

ed updated this object.
ed set the repository for this revision to rS FreeBSD src repository - subversion.

Rewrite tsearch()/tdelete() once more.

It turns out that we can come up with an iterative algorithm. Rotations can also be applied from top to bottom. Let's keep track of the path we took and use that to apply the rotations.

ed edited edge metadata.

A deterministic random number generator may be more useful for the test, so that runs can be reproduced.

Looks good to me otherwise.

In D4412#97496, @jilles wrote:

A deterministic random number generator may be more useful for the test, so that runs can be reproduced.

Looks good to me otherwise.

Which random number generator do you suggest should be used in this case? Are *rand48() good for this purpose?

In D4412#97602, @ed wrote:
In D4412#97496, @jilles wrote:

A deterministic random number generator may be more useful for the test, so that runs can be reproduced.

Looks good to me otherwise.

Which random number generator do you suggest should be used in this case? Are *rand48() good for this purpose?

*rand48() are not great but do allow proper seeding without global variables and should be good enough.

ed edited edge metadata.

Update the tests to make it easier to test deterministic input sequences.

In D4412#98210, @jilles wrote:
In D4412#97602, @ed wrote:

*rand48() are not great but do allow proper seeding without global variables and should be good enough.

Done! I think that for now it makes sense to pick the following middle ground:

  • Let the tests use nrand48().
  • Initialize the random state with arc4random_buf().

This means that we still have the advantage of not fixing the test to a single sequence (which may not provide full coverage), but still allow for changing the test to use a fixed sequence in case we want to narrow down problems. Is this acceptable?

In D4412#98339, @ed wrote:

Done! I think that for now it makes sense to pick the following middle ground:

  • Let the tests use nrand48().
  • Initialize the random state with arc4random_buf().

This means that we still have the advantage of not fixing the test to a single sequence (which may not provide full coverage), but still allow for changing the test to use a fixed sequence in case we want to narrow down problems. Is this acceptable?

If you insist. For the tests which are run automatically (e.g. by Jenkins) it is best to have them as deterministic as possible (even to the point of reducing coverage a little), so it is as easy as possible to diagnose failures and avoid Jenkins flapping. A test manually run by a developer need not be as deterministic and coverage is more important there.

This revision was automatically updated to reflect the committed changes.
In D4412#98740, @jilles wrote:

If you insist. For the tests which are run automatically (e.g. by Jenkins) it is best to have them as deterministic as possible (even to the point of reducing coverage a little), so it is as easy as possible to diagnose failures and avoid Jenkins flapping. A test manually run by a developer need not be as deterministic and coverage is more important there.

I've just checked in this change, using a fixed initialization for the randomizer state. I've left the arc4random_buf() available under an #if 0, so it can be enabled easily in case there is ever interest.