intmap: reimplement so that intmap_after works.
A critbit tree is a binary tree which keeps branches for each bit which differs in the leaves. It's a simple data structure, but not entirely simple to implement the primitives people expect, as this bus shows. The bug: I added an open iterator, and intmap_after_ for a random value would sometimes return the wrong node. Cause: we don't know what the prefix is as we iterate, so by only testing the critbits in the tree, we can end up in the wrong place. This is only a problem if the value isn't in the (sub)tree, but this can easily happen even with contiguous trees should deletion occur. You can see an example in the next patch, which adds a test. After finding a bug in my intmap_after() routine, I went searching for other implementations to see how they handled it. Most didn't provide an open-ended iterator like this, relying on callback iterators which don't allow deletion. Gah! The exception was https://github.com/blynn/blt/blob/master/blt.c#L179 which implements blt_ceil() which does this (if you add one to the key, at least). However, it does it by effectively finding a node, using that to derive the prefix, then walking down the tree again. That's pretty suboptimal. There are basically two choices if you want an efficient after() operation: to reimplement this approach with some optimizations (ie. keep branches as we descend, and when we get to the bottom and know the prefix, we know which branch to go down), or keep the bits which got to each node. The latter is more optimal, but less generally useful: for bit strings, for example, we could keep the bits in common on each node, rather than storing the entire string at the bottom. But in practice you'd be doing allocations to re-create the index if the caller wanted it. However, in this implementation our keys are 64 bits only, and we already use a u8 for the bit number: using a 64-bit value there consumes no more space (thanks to alignment). We can store the critbit by using the prefix capped by a bit: 0b10000...0000 means no prefix and highest bit is the critbit, and 0bxxxxx1000...000 means the prefix is xxxxxx and the critbit is the 6th highest bit. The penalty is that iteration 70% slower. It's still pretty fast though. Before: $ for i in `seq 5`; do ./speed 10000000; done | stats 10000000,random generation (nsec),3-4(3.2+/-0.4) 10000000,critbit insert (nsec),1530-1751(1633.2+/-80) 10000000,critbit successful lookup (nsec),1723-1993(1806.8+/-97) 10000000,critbit failed lookup (nsec),1763-2104(1933.6+/-1.3e+02) 10000000,critbit iteration (nsec),208-266(242.2+/-19) 10000000,critbit memory (bytes),48 10000000,critbit delete (nsec),1747-1861(1803.8+/-42) 10000000,critbit consecutive iteration (nsec),182-228(210+/-18) After: 10000000,random generation (nsec),3-4(3.2+/-0.4) 10000000,critbit insert (nsec),1533-1699(1628+/-65) 10000000,critbit successful lookup (nsec),1831-2104(1972.4+/-1e+02) 10000000,critbit failed lookup (nsec),1850-2152(2008.2+/-1.1e+02) 10000000,critbit iteration (nsec),304-324(312.8+/-7.5) 10000000,critbit memory (bytes),48 10000000,critbit delete (nsec),1617-1872(1752+/-99) 10000000,critbit consecutive iteration (nsec),303-318(311+/-5.4) Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
Showing
Please register or sign in to comment