Skip to content

A portable in-place bitwise binary Fredkin trie algorithm which allows for near constant time insertions, deletions, finds, closest fit finds and iteration. Is approx. 50-100% faster than red-black trees and up to 20% faster than O(1) hash tables.

andradeandrey/nedtries

 
 

Repository files navigation

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">

<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type" />
<title>nedtries Readme</title>
<style type="text/css">
<!--
body {
	text-align: justify;
}
h1, h2, h3, h4, h5, h6 {
	margin-bottom: -0.5em;
}
h1 {
	text-align: center;
}
h2 {
	text-decoration: underline;
	margin-bottom: -0.25em;
}
p {
	margin-top: 0.5em;
	margin-bottom: 0.5em;
}
pre {
	background: #eee;
	width: 80%;
	margin-left:auto;
	margin-right:auto;
}
ul li, ol li {
	margin-top: 0.2em;
	margin-bottom: 0.2em;
}
dl {
	margin-left: 2em;
}
dl dt {
	font-weight: bold;
}
dt + dd {
	margin-bottom: 1em;
}
.gitcommit {
	font-family: "Courier New", Courier, monospace;
	font-size: smaller;
}
-->
</style>
</head>

<body>

<div style="text-align: center">
	<h1 style="text-decoration: underline">nedtries v1.01 RC2 (?)</h1>
	<h2 style="text-decoration: none;">by Niall Douglas</h2>
	<p>Web site: <a href="http://www.nedprod.com/programs/portable/nedtries/">http://www.nedprod.com/programs/portable/nedtries/</a></p>
	<hr /></div>
<p>Enclosed is nedtries, an in-place bitwise binary Fredkin trie algorithm which allows for near 
constant time insertions, deletions, finds,
<span style="text-decoration: underline"> <strong>closest fit finds</strong></span> and iteration. On 
modern hardware it is approximately 50-100% faster than red-black binary trees, it 
handily beats even the venerable O(1) hash table for less than 3000 objects and 
it is barely slower than the hash table for 10000 objects. Past 10000 objects 
you probably ought to use a hash table though.</p>
<p>It is licensed under the
<a href="http://www.boost.org/LICENSE_1_0.txt" target="_blank">Boost Software License</a> 
which basically means you can do anything you like with it. Commercial support is 
available from <a href="http://www.nedproductions.biz/" target="_blank">ned 
Productions Limited</a>.</p>
<p>Its advantages over other algorithms are sizeable:</p>
<ol>
	<li>It has all the advantages of red-black trees such as nearest-fit finds 
	(i.e. find the item which is the closest to the search term) without 
	anything like the impact on memory bandwidth as red-black trees (i.e. it 
	scales far better with memory pressure than red-black trees).</li>
	<li>It doesn&#39;t require dynamic memory like hash tables, so it can be used in 
	a bounded environment such as a bootstrapper or a tiny embedded systems 
	kernel. It is also a lot faster than hash tables for less than a few 
	thousand items.</li>
	<li>Unlike either red-black trees or most hash tables, nedtries can store as 
	many items with identical keys as you like.</li>
	<li>Its performance is nearly perfectly stable over time and number of 
	contents N with a worst case complexity of O(M) following a mostly linear 
	degradation with increasing M average where 1 &lt;= M &lt;= 8*sizeof(void *). M is a measure 
	of the entropy between differing keys, so where keys are very similar at the 
	bit level M is 
	higher than where keys are very dissimilar.</li>
	<li>Its complexities for find and insert are identical, whereas for deletion 
	it is slightly more constant. Unlike almost any other algorithm, bitwise binary tries 
	have nearly identical real world speeds for ALL its operations rather than 
	being fast at one thing but slow at the others. In other words, if your code 
	equally inserts, deletes and finds with no preference for which then 	<span style="text-decoration: underline">this algorithm will beat all others 
	in the general purpose situation</span>.</li>
</ol>
<p>Its primary disadvantage is that it can only key upon a size_t (i.e. the size 
of a void *), so it cannot make use of an arbitrarily large key like a hash 
table can (though of course one could hash the large key into a size_t sized 
key). It also runs fastest when the key is as unique as possible, so if you wish 
to replace a red-black tree which has a complex left-right comparison function 
which cannot be converted into a stable size_t value then you will need to stick 
with red-black trees. In other words, it is ideal when you are keying on pointer 
sized keys where each item has a definitive non-changing key.</p>
<p>In short, <strong>if 
you are not already using bitwise tries then you probably ought to be!</strong> 
Read on for how to add them to your software.</p>
<div><center>
<img alt="Bitwise Trees Scaling" height="25%" src="images/BitwiseTreesScaling.png" width="25%" /><img alt="Red Black Trees Scaling" height="25%" src="images/RedBlackTreesScaling.png" width="25%" /><img alt="Hash Table Scaling" height="25%" src="images/HashTableScaling.png" width="25%" /></center></div>
<h2><a name="implementation">A. Implementation:</a></h2>
<p>The source makes use of C macros on C and C++ templates on C++ - therefore, 
unlike typical C-macro-based algorithms it is easy to debug and in fact, the improved 
metadata specified by the templates lets a modern C++ compiler produce 5-15% 
faster code through PGO guided selective inlining. The code is 100% standard C and C++, so it should run on any 
platform or architecture though you <em>may</em> need to implement your own nedtriebitscanr() 
function if you&#39;re not using GCC nor MSVC and want to keep performance high. If 
you are building debug, NEDTRIEDEBUG is by default turned on: this causes a 
complete state validation check to be performed after each and every change to 
the trie which tends to be very good at catching bugs early, but can make debug 
builds a little slow.</p>
<p>So what is &quot;an in-place bitwise binary Fredkin trie algorithm&quot; then? 
Well you ought to start by reading and fully digesting
<a href="http://en.wikipedia.org/wiki/Trie" target="_blank">the Wikipedia page 
on Fredkin tries</a> as what comes won&#39;t make much sense otherwise. The 
Wikipedia page describes a non-inplace trie which uses dynamic memory to store 
each consecutive non-differing section of a string, and indeed this is how tries 
are normally described in algorithm theory and classes. nedtries obviously 
enough selects on individual bits rather than substrings, and it uses an inplace 
instead of dynamically allocated implementation.</p>
<p>Here is how nedtries performs an indexation: firstly, the most significant 
set bit X is found using nedtriebitscanr() which is no more than one to three 
CPU cycles on modern processors. This is used to index an array of 
bins. Each bin X contains a binary tree of items whose keys are (1&lt;&lt;X) &lt;= key &lt; 
(1&lt;&lt;(X+1)), so what one does is to follow the tree downwards selecting left or 
right based on whether the next bit downwards is 0 or 1. If an item has 
children, its key is only guaranteed to be constrained to that of its bin, 
whereas if an item does not have children then its key is guaranteed to match as 
closely as possible its position in the tree.</p>
<p>If you insert an item, nedtries indexes as far as it can down the existing 
tree where the new item ought to be and inserts it there. If you remove an item, 
if that item has no children it is simply removed. If it has a child then a 
nobble function is called to select the bias for how to select the childless 
item to be nobbled and used as the replacement i.e. one either traverses down 
preferentially 1 or preferentially 0 until you find a childless item, then you 
delink it from there and link it in to replace the item being removed.</p>
<p>If you think about this hard enough, you realise that you will get a &quot;nearly 
sorted&quot; binary tree i.e. one whose node keys are very nearly in order. In fact, 
the more randomised the key, the more in order the tree becomes. The tree is 
usually sufficiently ordered that one can assume it to be so for most 
operations, but if you need to guarantee order then you can bubble sort per MSB 
bin as bubble sort performs <em>very</em> well on nearly sorted data (as does 
smooth sort if you have a very large set of data) .</p>
<p>The enclosed benchmark.cpp will run a series of scalability tests comparing the bitwise 
binary trie implementation from nedtries with others outputting its results in 
CSV format:</p>
<ul>
	<li>If compiled as C not C++, the C macro version of nedtries is compared to the red-black binary tree 
implementation from FreeBSD and the O(1) hash table implementation from
	<a href="http://uthash.sourceforge.net/">http://uthash.sourceforge.net/</a>.</li>
	<li>If compiled as C++, the C++ template version of nedtries is compared to 
	all of the C tests above as well as the STL associative container classes 
	std::map&lt;&gt; and std::unordered_map&lt;&gt;. NOTE THAT YOU NEED A TR1 SUPPORTING 
	COMPILER FOR std::unordered_map&lt;&gt; SUPPORT!</li>
</ul>
<p>You will also find enclosed a set of precomputed 
Microsoft Excel spreadsheets which were generated on a 2.67Ghz Intel Core 2 Quad 
Windows 7 x64 machine. They should be representative of performance on modern 
hardware - though note that the Intel Atom has a 17 cycle nedtriebitscanr() 
which is the only modern CPU to be so slow. See
<a href="http://gmplib.org/~tege/x86-timing.pdf">http://gmplib.org/~tege/x86-timing.pdf</a> for x86 and x64 instruction timings.</p>
<h2><a name="c-usage">B. C Macro Usage:</a></h2>
<p>Usage via C macros follows the FreeBSD <a href="rbtree.h">rbtree.h</a> 
format. See the enclosed <a href="nedtries.chm">nedtries.chm</a>  for detailed 
API documentation. Here is some sample code which can be compiled cleanly using <tt>gcc -Wall -pedantic 
-std=c99 test.c</tt> (or as C++ via <tt>g++ -Wall -pedantic test.c</tt>):</p>
<pre>#include &lt;stdio.h&gt;
#include &lt;string.h&gt;
#include &quot;nedtrie.h&quot;

typedef struct foo_s foo_t;
struct foo_s {
  NEDTRIE_ENTRY(foo_s) link;
  size_t key;
};
typedef struct foo_tree_s foo_tree_t;
NEDTRIE_HEAD(foo_tree_s, foo_s);
static foo_tree_t footree;

size_t fookeyfunct(const foo_t *r)
{
  return r-&gt;key;
}

NEDTRIE_GENERATE(static, foo_tree_s, foo_s, link, fookeyfunct, NEDTRIE_NOBBLEZEROS(foo_tree_s))

int main(void)
{
  foo_t a, b, c, *r;
  NEDTRIE_INIT(&amp;footree);
  a.key=2;
  NEDTRIE_INSERT(foo_tree_s, &amp;footree, &amp;a);
  b.key=6;
  NEDTRIE_INSERT(foo_tree_s, &amp;footree, &amp;b);
  r=NEDTRIE_FIND(foo_tree_s, &amp;footree, &amp;b);
  assert(r==&amp;b);
  c.key=5;
  r=NEDTRIE_NFIND(foo_tree_s, &amp;footree, &amp;c);
  assert(r==&amp;b); /* NFIND finds next largest. Invert the key function (i.e. 1-key) to find next smallest. */
  NEDTRIE_REMOVE(foo_tree_s, &amp;footree, &amp;a);
  NEDTRIE_FOREACH(r, foo_tree_s, &amp;footree)
  {
    printf(&quot;%p, %u\n&quot;, (void *) r, (unsigned) r-&gt;key);
  }
  assert(!NEDTRIE_PREV(foo_tree_s, &amp;footree, &amp;b));
  assert(!NEDTRIE_NEXT(foo_tree_s, &amp;footree, &amp;b));
  return 0;
}</pre>
<p>There isn&#39;t really much more to it - if you want to throw away the trie, 
simply NEDTRIE_INIT() its head. As no dynamic memory is involved, nothing is 
lost.</p>
<h3>Choosing The Nobble Function</h3>
<p>I should mention what the nobble function is for: you have three default 
choices, NEDTRIE_NOBBLEZEROS, NEDTRIE_NOBBLEONES and NEDTRIE_NOBBLEEQUALLY 
though you can of course also define your own. The nobble function contributes 
to tree balance by working <em>against</em> bit bias in your keys, so if your 
keys contain an excess of<em> non-leading </em>zeros then you should 
preferentially nobble zeros. Equally if your keys contain an excess of ones, 
then you should preferentially nobble ones and, as you might have guessed, if 
your bits <em>after the first set bit</em> are completely random (which is rare) 
then you should nobble equally.</p>
<p>Sounds complicated? In fact it&#39;s very easy if you simply use trial &amp; error. 
Start with nobble zeroes which tends to be right in most situations, and then 
use benchmarking your code to determine the correct setting.</p>
<h2><a name="changelog0">C. C++ Usage:</a></h2>
<p>C++ usage is even easier than the C macro usage thanks to nedtries::trie_map&lt;&gt; 
which is API compatible with the std::map&lt;&gt;, std::multimap&lt;&gt; and std::unordered_map&lt;&gt; 
STL associative containers. trie_map&lt;&gt; 
makes full use of rvalue construction if either you are running on C++0x 
according to the value of __cplusplus, or have 
defined HAVE_CPP0X. In the general case, simply drop trie_map&lt;&gt; in where 
your STL associative container used to be and enjoy the speed benefits!</p>
<p>Note that insertion and deletion speed in <strong>any</strong> STL container 
is heavily bound by the speed of your memory allocator. You may wish to consider 
employing
<a href="http://www.nedprod.com/programs/portables/nedmalloc/" target="_blank">
nedmalloc</a> which can deliver some unholy speed benefits if you run it as 
root, otherwise it will need some small source changes to employ its advanced v2 
malloc API.</p>
<p>In case you are not familiar with STL associative containers, they are very 
simple e.g.:</p>
<pre>nedtries::trie_map&lt;size_t, Foo&gt; foomap;
foomap[5]=Foo();
foomap.erase(foomap.find(5));</pre>
<p>You can of course iterate through them and do all the normal things you can 
do with any STL container.</p>
<h3>The trie_map&lt;&gt; Implementation</h3>
<p>trie_map&lt;&gt; is actually a STL container <em>wrapper</em> rather than a proper STL 
container in its own right i.e. it subclasses an existing STL container passing 
through most of its API, but selectively overrides certain members. Its default 
parameters point at std::list&lt;&gt; which is its most likely usage model for most 
people.</p>
<p>The advantages are mainly that it is quick to implement and can be 
theoretically applied to any arbitrary STL container, thus taking advantage of 
that STL container&#39;s optimisations and customisations. The big disadvantage is 
that it is hacky, dirty and prone to getting bugs into it, and if you look at 
the source you&#39;ll see what I mean. There is after all a number of places where I 
am doing a number of very illegal things in C++ which just happen to usually 
work.</p>
<p>The chances are that this implementation will be good enough for most people. 
If however you might like to sponsor the development of a full bitwise trie STL 
associative container for submission to
<a href="http://www.boost.org/" target="_blank">the Boost C++ peer reviewed 
libraries</a> (and thereafter into the standard C++ language itself), I would be 
very pleased to oblige. <a href="http://www.nedproductions.biz/" target="_blank">
Please contact ned Productions Consulting Ltd. for further details</a>.</p>
<h2><a name="changelog">D. ChangeLog:</a></h2>
<h3>v1.01 RC2 (?):</h3>
<ul>
	<li><span class="gitcommit">[master bd6f3e5]</span> Fixed misc documentation errors.</li>
	<li><span class="gitcommit">[master 79efb3a]</span> Fixed really obvious 
	documentation bug in the example usage. Thanks to Fabian Holler for 
	reporting this.</li>
	<li><span class="gitcommit">[master xxxxxxx]</span> Fixed that the example 
	usage in the documentation spews warnings on GCC. Now compiles totally 
	cleanly. Thanks to Fabian Holler for 
	reporting this.</li>
</ul>
<h3>v1.01 RC1 (19th June 2011):</h3>
<ul>
	<li><span class="gitcommit">[master 30a440a]</span> Fixed misc documentation errors.</li>
	<li><span class="gitcommit">[master 2103969]</span> Fixed misoperation when trie key is
zero. Thanks to Andrea for reporting this.</li>
	<li><span class="gitcommit">[master 083d94b]</span> Added support for MSVC's as old as 7.1.</li>
	<li><span class="gitcommit">[master f836319]</span> Added Microsoft CLR target support.</li>
	<li><span class="gitcommit">[master 85abf67]</span> I, being a muppet of the highest
order, was actually benchmarking the speed of the timing routines rather than much
else. Performance is now approx. 10x higher in the graphs ... I am a fool!</li>
	<li><span class="gitcommit">[master 6aa344e]</span> Added check for key uniqueness
in benchmark test (hash tables suffer is key isn't unique). Added cube root averaging
to results output.</li>
	<li><span class="gitcommit">[master feb4f56]</span> Replaced the use of rand()
with the Mersenne Twister (<a href="http://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/SFMT/index.html">
http://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/SFMT/index.html</a>).</li>
</ul>
<h3>v1.00 beta 1 (18th June 2010):</h3>
<ul>
	<li><span class="gitcommit">[master e4d1245]</span> First release.</li>
</ul>

</body>

</html>

About

A portable in-place bitwise binary Fredkin trie algorithm which allows for near constant time insertions, deletions, finds, closest fit finds and iteration. Is approx. 50-100% faster than red-black trees and up to 20% faster than O(1) hash tables.

Resources

Stars

Watchers

Forks

Packages

No packages published