Skip to content

Commit d2e207e

Browse files
committed
faster insert wip
1 parent f8b0621 commit d2e207e

8 files changed

Lines changed: 314 additions & 1 deletion

File tree

roaringbitmap/src/main/java/org/roaringbitmap/art/Art.java

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
package org.roaringbitmap.art;
22

33
import org.roaringbitmap.ArraysShim;
4+
import org.roaringbitmap.Container;
45
import org.roaringbitmap.longlong.LongUtils;
56

67
import java.io.DataInput;
78
import java.io.DataOutput;
89
import java.io.IOException;
910
import java.nio.ByteBuffer;
11+
import java.util.function.Supplier;
12+
import java.util.function.ToLongFunction;
13+
14+
import static org.roaringbitmap.art.BranchNode.ILLEGAL_IDX;
1015

1116
/**
1217
* See: https://db.in.tum.de/~leis/papers/ART.pdf a cpu cache friendly main memory data structure.
@@ -222,6 +227,124 @@ protected Toolkit removeSpecifyKey(Node node, byte[] key, int dep) {
222227
return null;
223228
}
224229

230+
/**
231+
* Find or create a leaf node by the high part of the key.
232+
* If the leaf node is not found, it will be created with the value returned by
233+
* nextContainer.applyAsLong(ifNotFound).
234+
*
235+
* @param highPart the high part of the key
236+
* @param nextContainer a function to get the next container index
237+
* @param ifNotFound a supplier to provide a value if the key is not found
238+
* @return the container index of the found or created leaf node
239+
*/
240+
public <T> long findOrCreateByKey(long highPart, ToLongFunction<Supplier<T>> nextContainer, Supplier<T> ifNotFound) {
241+
LeafNode result;
242+
243+
if (root == null) {
244+
result = new LeafNode(highPart, nextContainer.applyAsLong(ifNotFound));
245+
root = result;
246+
return result.getContainerIdx();
247+
}
248+
byte depth = 0;
249+
byte parentKeyInGrandParent = 0;
250+
BranchNode grandParent = null;
251+
Node parent = root;
252+
Node originalParent;
253+
// on each cycle
254+
// if gParent == null, parent will be the next root
255+
// if gParent != null, parent should replace grandParent at key parentKeyInGrandParent
256+
// depth is the depth of parent in the trie
257+
// result is the leaf node, or null if we are just finding
258+
259+
//Parent can grow, so we need to keep track of the parent index in grandParent
260+
// but grandParent cant.
261+
while (true) {
262+
//keep a copy of the original parent, so we can see if it changes, and adjust the tree
263+
originalParent = parent;
264+
265+
//usually a branch node, so lets test that first
266+
if (parent instanceof BranchNode) {
267+
BranchNode parentBranch = (BranchNode) parent;
268+
//is parent the real parent - does the prefix match?
269+
byte prefixLength = parentBranch.prefixLength();
270+
if (prefixLength > 0) {
271+
byte matchLength = prefixMatchLength(LongUtils.fromArray(parentBranch.prefix), depth, prefixLength, highPart);
272+
if (matchLength == prefixLength) {
273+
// prefix matches, so we can just continue
274+
depth += prefixLength;
275+
continue;
276+
} else {
277+
//so we have a partial match, we need to split the branch
278+
// for example, if the prefix is [1,2,3] and the highPart is 0xab99010204, and depth 2
279+
// we have a match length of 2,
280+
// so we create a new parent with prefix is [1,2]
281+
// with 2 children - the current parent prefix shrunk to [], at key 3
282+
// and add a leaf node with key 4
283+
byte branchKey = parentBranch.prefix[matchLength];
284+
parentBranch = parentBranch.shrinkPrefixBy(matchLength + 1);
285+
result = new LeafNode(highPart, nextContainer.applyAsLong(ifNotFound));
286+
parent = Node4.create(parentBranch, result, branchKey, LongUtils.getByte(highPart, depth + matchLength), highPart, depth, (byte) depth + matchLength);
287+
break;
288+
}
289+
}
290+
//OK so parent is ok, and depth is adjusted. Let try to move to the next level
291+
292+
byte childKey = LongUtils.getByte(highPart, depth);
293+
// We optimise for the case where the childKey is already present at this level, and we are walking a tree
294+
// it should be the common case, so we try to find the child at this level
295+
//we could calculate the position for the access, but that means that we have to have 2 accesses on the common path,
296+
// so they shoul dbe faster
297+
Node childNode = parentBranch.getChildAtKey(childKey);
298+
if (childNode == null) {
299+
result = new LeafNode(highPart, nextContainer.applyAsLong(ifNotFound));
300+
parent = parentBranch.insert(result, childKey);
301+
break;
302+
} else {
303+
grandParent = parentBranch;
304+
parentKeyInGrandParent = childKey;
305+
parent = childNode;
306+
depth += 1;
307+
308+
}
309+
} else {
310+
LeafNode leafNode = (LeafNode) parent;
311+
long leafNodeKey = leafNode.getKey();
312+
if (leafNodeKey == highPart) {
313+
result = leafNode;
314+
} else {
315+
//we have to create a new Node4 with the two leaves
316+
result = new LeafNode(highPart, nextContainer.applyAsLong(ifNotFound));
317+
byte matchLength = prefixMatchLength(leafNodeKey, depth, (byte)6, highPart);
318+
319+
// create a new parent node4 with the current leaf node and the new leaf node
320+
Node4 split = Node4.create(leafNode, result,
321+
LongUtils.getByte(leafNodeKey, depth + matchLength), LongUtils.getByte(highPart, depth + matchLength),
322+
highPart, depth, (byte) (depth + matchLength));
323+
parent = split;
324+
}
325+
break;
326+
}
327+
}
328+
if (grandParent == null) {
329+
root = parent;
330+
} else if (parent != originalParent) {
331+
// if the parent has changed, we need to replace it in the grandParent
332+
int pos = grandParent.getChildPos(parentKeyInGrandParent);
333+
grandParent.replaceNode(pos, parent);
334+
}
335+
return result.getContainerIdx();
336+
}
337+
338+
private byte prefixMatchLength(long prefix, byte depth, byte prefixLength, long highPart) {
339+
for (byte i = 0; i < prefixLength; i++) {
340+
if (LongUtils.getByte(prefix,i) != LongUtils.getByte(highPart, depth + i)) {
341+
return i;
342+
}
343+
}
344+
return prefixLength;
345+
}
346+
347+
225348
class Toolkit {
226349

227350
Node freshMatchedParentNode; // indicating a fresh parent node while the original

roaringbitmap/src/main/java/org/roaringbitmap/art/BranchNode.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import java.io.DataOutput;
44
import java.io.IOException;
55
import java.nio.ByteBuffer;
6+
import java.util.Arrays;
67

78
public abstract class BranchNode extends Node {
89

@@ -233,5 +234,21 @@ protected int serializeHeaderSizeInBytes() {
233234
return super.serializeHeaderSizeInBytes() + prefixLength();
234235
}
235236

236-
237+
/**
238+
* shrink the prefix by the specified number of bytes
239+
* @param bytes the number of bytes to shrink, must be greater than 0 and less than or equal to the current prefix length
240+
* @return this node, or an equivenent node with a smaller prefix
241+
*/
242+
BranchNode shrinkPrefixBy(int bytes) {
243+
assert bytes > 0 && bytes <= prefixLength();
244+
byte prefixLength = this.prefixLength();
245+
if (bytes == prefixLength) {
246+
// shrink to empty prefix
247+
this.prefix = Art.EMPTY_BYTES;
248+
} else {
249+
prefix = Arrays.copyOfRange(prefix, bytes, prefixLength);
250+
}
251+
//in the future we may remove the prefix as an array, and in that case the node may be different
252+
return this;
253+
}
237254
}

roaringbitmap/src/main/java/org/roaringbitmap/art/Node4.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,40 @@ public class Node4 extends BranchNode {
1616
public Node4(int compressedPrefixSize) {
1717
super(compressedPrefixSize);
1818
}
19+
public static Node4 create(Node node1, Node node2, byte node1Key, byte node2Key,
20+
long prefixSource, byte prefixStart, byte prefixlength) {
21+
if ((char)node1Key > (char)node2Key) {
22+
return createOrdered(node1, node2, node1Key, node2Key, prefixSource, prefixStart, prefixlength);
23+
} else {
24+
return createOrdered(node2, node1, node2Key, node1Key, prefixSource, prefixStart, prefixlength);
25+
}
26+
}
27+
/**
28+
* create a Node4 with two child nodes and their keys.
29+
* It is required that node1Key < node2Key when conpared as unsigned bytes.
30+
* The child nodes must have the prefixes adjusted before callingt this method.
31+
* @param node1 the first node
32+
* @param node2 the second node
33+
* @param node1Key the key of the first node
34+
* @param node2Key the key of the second node
35+
* @param prefixSource the source of the prefix, in big endian order
36+
* @param prefixStart the start index of the prefix in the source
37+
* @param prefixLength the end index of the prefix in the source
38+
* @return a Node4 instance with the two nodes as children
39+
*/
40+
public static Node4 createOrdered(Node node1, Node node2, byte node1Key, byte node2Key,
41+
long prefixSource, byte prefixStart, byte prefixLength) {
42+
Node4 node4 = new Node4(prefixLength);
43+
node4.count = 2;
44+
node4.key = IntegerUtil.init2Bytes(node1Key, node2Key);
45+
node4.children[0] = node1;
46+
node4.children[1] = node2;
47+
for (int i = prefixStart; i < prefixStart + prefixLength; i++) {
48+
node4.prefix[i - prefixStart] = LongUtils.getByte(prefixSource, i);
49+
}
50+
return node4;
51+
}
52+
1953

2054
@Override
2155
protected NodeType nodeType() {

roaringbitmap/src/main/java/org/roaringbitmap/longlong/HighLowContainer.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import java.io.IOException;
1717
import java.nio.ByteBuffer;
1818
import java.util.NoSuchElementException;
19+
import java.util.function.Supplier;
1920

2021
public class HighLowContainer {
2122

@@ -72,6 +73,12 @@ public void put(byte[] highPart, Container container) {
7273
art.insert(highPart, containerIdx);
7374
}
7475

76+
public ContainerWithIndex findOrCreateContainer(long highPart, Supplier<Container> ifNotFound) {
77+
long containerIdx = art.findOrCreateByKey(highPart, containers::addContainer, ifNotFound);
78+
Container container = containers.getContainer(containerIdx);
79+
return new ContainerWithIndex(container, containerIdx);
80+
}
81+
7582
/**
7683
* Attempt to remove the container that corresponds to the 48 bit key.
7784
* @param highPart the 48 bit key
@@ -325,4 +332,5 @@ public boolean equals(Object object) {
325332
}
326333
return false;
327334
}
335+
328336
}

roaringbitmap/src/main/java/org/roaringbitmap/longlong/IntegerUtil.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ public static int setByte(int v, byte bv, int pos) {
4141
v |= (bv & 0xFF) << i;
4242
return v;
4343
}
44+
public static int init2Bytes(byte b1, byte b2) {
45+
return (b1 << 24) | ((b2 & 0xFF) << 16);
46+
}
4447

4548
/**
4649
* shift the byte left from the specified position

roaringbitmap/src/main/java/org/roaringbitmap/longlong/LongUtils.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,19 @@ public static long fromKey(byte[] key) {
114114
| (long) (key[4] & 0xff) << 24
115115
| (long) (key[5] & 0xff) << 16;
116116
}
117+
/**
118+
* get the long from the big endian representation bytes
119+
*
120+
* @param key the byte array. Max length 6 bytes
121+
* @return the long data
122+
*/
123+
public static long fromArray(byte[] key) {
124+
long result = 0;
125+
for (int i = 0; i < key.length; i++) {
126+
result |= ((long) (key[i] & 0xff)) << ((5 - i) * 8);
127+
}
128+
return result;
129+
}
117130

118131
/**
119132
* initialize a long value with the given fist 32 bit

roaringbitmap/src/main/java/org/roaringbitmap/longlong/Roaring64Bitmap.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,14 @@ public void addLong(long x) {
6767
highLowContainer.put(high, arrayContainer);
6868
}
6969
}
70+
public void addLongNew(long x) {
71+
long high = LongUtils.highPartOnly(x);
72+
char low = LongUtils.lowPart(x);
73+
ContainerWithIndex containerWithIndex = highLowContainer.searchOrCreateContainer(high, ArrayContainer::new);
74+
Container container = containerWithIndex.getContainer();
75+
Container freshOne = container.add(low);
76+
highLowContainer.replaceContainer(containerWithIndex.getContainerIdx(), freshOne);
77+
}
7078

7179
/**
7280
* Returns the number of distinct integers added to the bitmap (e.g., number of bits set).
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package bench;
2+
3+
import org.roaringbitmap.longlong.Roaring64Bitmap;
4+
5+
import java.util.Arrays;
6+
import java.util.Random;
7+
import com.sun.management.ThreadMXBean;
8+
import java.lang.management.ManagementFactory;
9+
import java.util.function.Supplier;
10+
11+
public class SimpleLookup {
12+
13+
public static void main(String[] args) throws Exception {
14+
SimpleLookup lookup = new SimpleLookup();
15+
16+
// lookup.runBenchmark(lookup::lookupPopulated, "lookupPopulated", 5);
17+
lookup.runBenchmark(lookup::addSorted, "addSorted", 5);
18+
// lookup.runBenchmark(lookup::addUnsorted, "addUnsorted", 5);
19+
20+
// lookup.runBenchmark(lookup::addSortedSmall, "addSortedSmall", 50);
21+
// lookup.runBenchmark(lookup::addUnsortedSmall, "addUnsortedSmall", 50);
22+
}
23+
24+
private void runBenchmark(Supplier<Object> fn, String name, int runs) {
25+
long[] durations = new long[runs];
26+
long[] allocations = new long[runs];
27+
Object[] results = new Object[runs];
28+
long currentThreadId = Thread.currentThread().getId();
29+
ThreadMXBean threadMxBean = (ThreadMXBean)
30+
ManagementFactory.getThreadMXBean();
31+
32+
for (int i = 0; i < 5; i++) {
33+
System.out.println("Running " + name + " benchmark warmup " + i);
34+
results[i] = fn.get();
35+
}
36+
37+
for (int i = 0; i < runs; i++) {
38+
System.out.println("Running " + name + " benchmark iteration " + i);
39+
long allocBefore = threadMxBean.getThreadAllocatedBytes(currentThreadId);
40+
long startTime = System.nanoTime();
41+
Object result = fn.get();
42+
long endTime = System.nanoTime();
43+
long allocAfter = threadMxBean.getThreadAllocatedBytes(currentThreadId);
44+
durations[i] = endTime - startTime;
45+
allocations[i] = allocAfter - allocBefore;
46+
results[i] = result;
47+
System.out.println("Iteration " + i + ": Duration: " + durations[i] + " ns, Allocated: " + allocations[i] + " bytes, Result: " + results[i]);
48+
}
49+
System.out.println("Average Duration: " + Arrays.stream(durations).asDoubleStream().map(x -> x / 1000000).summaryStatistics() + " ms");
50+
System.out.println("Average Allocation: " + Arrays.stream(allocations).asDoubleStream().map(x -> x / 1024 / 1024).summaryStatistics() + " MB");
51+
52+
}
53+
54+
// private Roaring64Bitmap testPopulated = new Roaring64Bitmap();
55+
private SimpleLookup() {
56+
// for (long index : indexes) {
57+
// testPopulated.addLong(index);
58+
// }
59+
}
60+
61+
private long[] indexes = new Random(0L).longs().limit(10000000).toArray();
62+
private long[] sortedIndexes = Arrays.stream(indexes).sorted().toArray();
63+
64+
private long[] indexesSmall = new Random(0L).longs().limit(100000).toArray();
65+
private long[] sortedIndexesSmall = Arrays.stream(indexesSmall).sorted().toArray();
66+
67+
68+
// public int lookupPopulated() {
69+
// int result = 0;
70+
// for (long index : indexes) {
71+
// if (testPopulated.contains(index)) {;
72+
// result++;
73+
// }
74+
// }
75+
// return result;
76+
// }
77+
78+
public Boolean addSorted() {
79+
Roaring64Bitmap test = new Roaring64Bitmap();
80+
for (long index : sortedIndexes) {
81+
test.addLong(index);
82+
}
83+
return test.isEmpty();
84+
}
85+
public Boolean addUnsorted() {
86+
Roaring64Bitmap test = new Roaring64Bitmap();
87+
for (long index : indexes) {
88+
test.addLong(index);
89+
}
90+
return test.isEmpty();
91+
}
92+
public Boolean addSortedSmall() {
93+
Roaring64Bitmap test = new Roaring64Bitmap();
94+
for (long index : sortedIndexesSmall) {
95+
test.addLong(index);
96+
}
97+
return test.isEmpty();
98+
}
99+
public Boolean addUnsortedSmall() {
100+
Roaring64Bitmap test = new Roaring64Bitmap();
101+
for (long index : indexesSmall) {
102+
test.addLong(index);
103+
}
104+
return test.isEmpty();
105+
}
106+
107+
}

0 commit comments

Comments
 (0)