#pragma intrinsic(memcmp) #include "flow/flow.h" #include "flow/DeterministicRandom.h" #include "PrefixTree.h" #include static int nextPowerOfTwo(int n) { int p; for (p = 1; p < n; p += p); return p; } static int less(StringRef a, StringRef b) { int al = a.size(), bl = b.size(); int cl = al < bl ? al : bl; uint8_t const* ap = a.begin(); uint8_t const* bp = b.begin(); for (int i = 0; i < cl; i++) { if (ap[i] < bp[i]) return 1; else if (bp[i] < ap[i]) return 0; } return al < bl; } struct CompactPreOrderTree { enum { ENABLE_PREFETCH_RIGHT=1 }; // Use rather more memory BW, but hide a little latency when a right branch takes us out of a cache line. Seems to help slightly. struct Node { enum { ENABLE_PREFIX=1 }; // Enable or disable key prefix compression within a CompactPreOrderTree enum { ENABLE_LEFT_PTR = 0 }; // offsets relative to `this`: enum { KEY_LENGTH_OFFSET = ENABLE_PREFIX*1 }; enum { KEY_DATA_OFFSET = KEY_LENGTH_OFFSET+1 }; // offsets relative to `keyEnd()`: enum { LPTR_OFFSET = 0 }; enum { RPTR_OFFSET = 2*ENABLE_LEFT_PTR }; enum { END_OFFSET = RPTR_OFFSET + 2 }; enum { IMPLICIT_LPTR_VALUE = END_OFFSET }; static int getMaxOverhead() { return KEY_DATA_OFFSET + END_OFFSET; } int keyPrefixLength() { if (ENABLE_PREFIX) return *(uint8_t*)this; else return 0; } int keyLength() { return *((uint8_t*)this + KEY_LENGTH_OFFSET); } uint8_t const* keyData() { return (uint8_t const*)this + KEY_DATA_OFFSET; } uint8_t const* keyEnd() { return (uint8_t const*)this + KEY_DATA_OFFSET + keyLength(); } StringRef key() { return StringRef(keyData(), keyLength()); } Node* left() { auto ke = keyEnd(); return (Node*)(ke + (ENABLE_LEFT_PTR ? *(int16_t*)(ke + LPTR_OFFSET) : IMPLICIT_LPTR_VALUE)); } Node* right() { auto ke = keyEnd(); return (Node*)(ke + *(uint16_t*)(ke + RPTR_OFFSET)); } uint8_t* getEnd() { return (uint8_t*)keyEnd() + END_OFFSET; } void setKeyPrefixLength(int l) { if (ENABLE_PREFIX) { ASSERT(l < 256); *(uint8_t*)this = l; } else ASSERT(!l); } void setKeyLength(int l) { ASSERT(l < 256); *((uint8_t*)this + KEY_LENGTH_OFFSET) = l; } void setLeftPointer(Node* ptr) { auto ke = keyEnd(); int o = (uint8_t*)ptr - ke; ASSERT(ENABLE_LEFT_PTR ? (int16_t(o) == o) : o == IMPLICIT_LPTR_VALUE); if (ENABLE_LEFT_PTR) *(uint16_t*)(ke + LPTR_OFFSET) = o; } void setRightPointer(Node* ptr) { auto ke = keyEnd(); int o = (uint8_t*)ptr - ke; ASSERT(-32768 <= o && o < 32767); *(uint16_t*)(ke + RPTR_OFFSET) = o; } }; int nodeCount; Node root; int relAddr(Node* n) { return (uint8_t*)n - (uint8_t*)this; } Node* lastLessOrEqual(StringRef searchKey) { Node* n = &root; // n is the root of the subtree we are searching Node* b = 0; // b is the greatest node <= searchKey which is a parent of n int nBFIndex = 0; // the index of the node n in the entire tree in "breadth first order", i.e. level by level. This is NOT the order the tree is stored in! int prefixSize = 0; // the number of bytes of searchKey which are equal to the first bytes of the logical key of the parent of n int dir; while (nBFIndex < nodeCount) { int np = n->keyPrefixLength(); if (ENABLE_PREFETCH_RIGHT) _mm_prefetch((const char*)n->right(), _MM_HINT_T0); if (prefixSize < np) { // The searchKey differs from this node's logical key in the prefix this node shares with its parent // So the comparison between this node and searchKey has the same result as the comparison with the parent and searchKey // (dir is unchanged) } else { // The searchKey is equal to this node's logical key up to the beginning of the compressed key int al = searchKey.size() - np; int bl = n->keyLength(); int cl = al < bl ? al : bl; int prefixLen = commonPrefixLength(searchKey.begin()+np, n->keyData(), cl); dir = prefixLen == cl ? alkeyData()[prefixLen]; if (Node::ENABLE_PREFIX) prefixSize = np + prefixLen; } nBFIndex = nBFIndex + nBFIndex + 2 - dir; auto l = n->left(), r = n->right(); b = dir ? b : n; n = dir ? l : r; } return b; } static std::pair lastLessOrEqual2(CompactPreOrderTree* this1, CompactPreOrderTree* this2, StringRef searchKey1, StringRef searchKey2) { // Do two separate lastLessOrEqual operations at once, to make better use of the memory subsystem. // Don't try to read this code, it is write only (constructed by copy/paste from lastLessOrEqual and adding 1 and 2 to variables as necessary) Node* n1 = &this1->root; // n is the root of the subtree we are searching Node* b1 = 0; // b is the greatest node <= searchKey which is a parent of n int nBFIndex1 = 0; // the index of the node n in the entire tree in "breadth first order", i.e. level by level. This is NOT the order the tree is stored in! int prefixSize1 = 0; // the number of bytes of searchKey which are equal to the first bytes of the logical key of the parent of n int dir1; Node* n2 = &this2->root; // n is the root of the subtree we are searching Node* b2 = 0; // b is the greatest node <= searchKey which is a parent of n int nBFIndex2 = 0; // the index of the node n in the entire tree in "breadth first order", i.e. level by level. This is NOT the order the tree is stored in! int prefixSize2 = 0; // the number of bytes of searchKey which are equal to the first bytes of the logical key of the parent of n int dir2; while (nBFIndex1 < this1->nodeCount && nBFIndex2 < this2->nodeCount) { int np1 = n1->keyPrefixLength(); int np2 = n2->keyPrefixLength(); if (ENABLE_PREFETCH_RIGHT) { _mm_prefetch((const char*)n1->right(), _MM_HINT_T0); _mm_prefetch((const char*)n2->right(), _MM_HINT_T0); } if (prefixSize1 < np1) { // The searchKey differs from this node's logical key in the prefix this node shares with its parent // So the comparison between this node and searchKey has the same result as the comparison with the parent and searchKey // (dir is unchanged) } else { // The searchKey is equal to this node's logical key up to the beginning of the compressed key int al1 = searchKey1.size() - np1; int bl1 = n1->keyLength(); int cl1 = al1 < bl1 ? al1 : bl1; int prefixLen1 = commonPrefixLength(searchKey1.begin() + np1, n1->keyData(), cl1); dir1 = prefixLen1 == cl1 ? al1keyData()[prefixLen1]; prefixSize1 = np1 + prefixLen1; } if (prefixSize2 < np2) { // The searchKey differs from this node's logical key in the prefix this node shares with its parent // So the comparison between this node and searchKey has the same result as the comparison with the parent and searchKey // (dir is unchanged) } else { // The searchKey is equal to this node's logical key up to the beginning of the compressed key int al2 = searchKey2.size() - np2; int bl2 = n2->keyLength(); int cl2 = al2 < bl2 ? al2 : bl2; int prefixLen2 = commonPrefixLength(searchKey2.begin() + np2, n2->keyData(), cl2); dir2 = prefixLen2 == cl2 ? al2keyData()[prefixLen2]; prefixSize2 = np2 + prefixLen2; } nBFIndex1 = nBFIndex1 + nBFIndex1 + 2 - dir1; nBFIndex2 = nBFIndex2 + nBFIndex2 + 2 - dir2; auto l1 = n1->left(), r1 = n1->right(); auto l2 = n2->left(), r2 = n2->right(); b1 = dir1 ? b1 : n1; b2 = dir2 ? b2 : n2; n1 = dir1 ? l1 : r1; n2 = dir2 ? l2 : r2; } while (nBFIndex1 < this1->nodeCount) { int np1 = n1->keyPrefixLength(); if (prefixSize1 < np1) { // The searchKey differs from this node's logical key in the prefix this node shares with its parent // So the comparison between this node and searchKey has the same result as the comparison with the parent and searchKey // (dir is unchanged) } else { // The searchKey is equal to this node's logical key up to the beginning of the compressed key int al1 = searchKey1.size() - np1; int bl1 = n1->keyLength(); int cl1 = al1 < bl1 ? al1 : bl1; int prefixLen1 = commonPrefixLength(searchKey1.begin() + np1, n1->keyData(), cl1); dir1 = prefixLen1 == cl1 ? al1keyData()[prefixLen1]; prefixSize1 = np1 + prefixLen1; } nBFIndex1 = nBFIndex1 + nBFIndex1 + 2 - dir1; auto l1 = n1->left(), r1 = n1->right(); b1 = dir1 ? b1 : n1; n1 = dir1 ? l1 : r1; } while (nBFIndex2 < this2->nodeCount) { int np2 = n2->keyPrefixLength(); if (prefixSize2 < np2) { // The searchKey differs from this node's logical key in the prefix this node shares with its parent // So the comparison between this node and searchKey has the same result as the comparison with the parent and searchKey // (dir is unchanged) } else { // The searchKey is equal to this node's logical key up to the beginning of the compressed key int al2 = searchKey2.size() - np2; int bl2 = n2->keyLength(); int cl2 = al2 < bl2 ? al2 : bl2; int prefixLen2 = commonPrefixLength(searchKey2.begin() + np2, n2->keyData(), cl2); dir2 = prefixLen2 == cl2 ? al2keyData()[prefixLen2]; prefixSize2 = np2 + prefixLen2; } nBFIndex2 = nBFIndex2 + nBFIndex2 + 2 - dir2; auto l2 = n2->left(), r2 = n2->right(); b2 = dir2 ? b2 : n2; n2 = dir2 ? l2 : r2; } return std::make_pair(b1,b2); } #if 0 enum { ENABLE_FANCY_BUILD=1 }; struct BuildInfo { Node* parent; bool rightChild; std::string const& prefix; std::string* begin; std::string* end; BuildInfo(Node* parent, bool rightChild, std::string const& prefix, std::string* begin, std::string* end) : parent(parent), rightChild(rightChild), prefix(prefix), begin(begin), end(end) {} }; int build(std::vector& input, std::string const& prefix = std::string()) { nodeCount = input.size(); Deque< BuildInfo > queue; Deque< BuildInfo > deferred; queue.push_back(BuildInfo(NULL, false, prefix, &input[0], &input[0] + input.size())); Node* node = &root; uint8_t* cacheLineEnd = (uint8_t*)node + 64; while (queue.size() || deferred.size()) { if (!queue.size()) { for (int i = 0; i < deferred.size(); i++) queue.push_back( deferred[i] ); deferred.clear(); } BuildInfo bi = queue.front(); queue.pop_front(); int mid = perfectSubtreeSplitPoint(bi.end - bi.begin); std::string& s = bi.begin[mid]; int prefixLen = Node::ENABLE_PREFIX ? commonPrefixLength((uint8_t*)&bi.prefix[0], (uint8_t*)&s[0], std::min(bi.prefix.size(), s.size())) : 0; node->setKeyPrefixLength(prefixLen); node->setKeyLength(s.size() - prefixLen); memcpy((uint8_t*)node->key().begin(), &s[prefixLen], s.size() - prefixLen); if (bi.parent) { if (bi.rightChild) bi.parent->setRightPointer(node); else bi.parent->setLeftPointer(node); } if ((uint8_t*)node->getEnd() > cacheLineEnd) { cacheLineEnd = (uint8_t*)((intptr_t)node->getEnd() &~63) + 64; for (int i = 0; i < queue.size(); i++) deferred.push_back(queue[i]); queue.clear(); } if (bi.begin != bi.begin + mid) queue.push_back(BuildInfo(node, false, s, bi.begin, bi.begin + mid)); else if (Node::ENABLE_LEFT_PTR) node->setLeftPointer(node); if (bi.begin + mid + 1 != bi.end) queue.push_back(BuildInfo(node, true, s, bi.begin + mid + 1, bi.end)); else node->setRightPointer(node); node = (Node*)node->getEnd(); } return (uint8_t*)node - (uint8_t*)this; } #else enum { ENABLE_FANCY_BUILD=0 }; int build(std::vector& input, std::string const& prefix = std::string()) { nodeCount = input.size(); return (uint8_t*)build(root, prefix, &input[0], &input[0]+input.size()) - (uint8_t*)this; } Node* build(Node& node, std::string const& prefix, std::string* begin, std::string* end) { if (begin == end) return &node; int mid = perfectSubtreeSplitPoint(end-begin); std::string& s = begin[mid]; int prefixLen = Node::ENABLE_PREFIX ? commonPrefixLength((uint8_t*)&prefix[0], (uint8_t*)&s[0], std::min(prefix.size(), s.size())) : 0; //printf("Node: %s at %d, subtree size %d, mid=%d, prefix %d\n", s.c_str(), relAddr(&node), end-begin, mid, prefixLen); node.setKeyPrefixLength(prefixLen); node.setKeyLength(s.size()-prefixLen); memcpy((uint8_t*)node.key().begin(), &s[prefixLen], s.size()-prefixLen); Node* next = (Node*)node.getEnd(); if (begin != begin + mid) { node.setLeftPointer(next); next = build(*node.left(), s, begin, begin + mid); } else if (Node::ENABLE_LEFT_PTR) node.setLeftPointer(&node); if (begin + mid + 1 != end) { node.setRightPointer(next); next = build(*node.right(), s, begin + mid + 1, end); } else node.setRightPointer(&node); return next; } #endif }; std::string printable(const StringRef& val) { std::string s; for (int i = 0; i= 32 && b < 127 && b != '\\') s += (char)b; else if (b == '\\') s += "\\\\"; else s += format("\\x%02x", b); } return s; } void compactMapTests(std::vector testData, std::vector sampleQueries, std::string prefixTreeDOTFile = "") { double t1, t2; int r = 0; std::sort(testData.begin(), testData.end()); /*for (int i = 0; i < testData.size() - 1; i++) { ASSERT(testData[i + 1].substr(0, 4) != testData[i].substr(0, 4)); ASSERT(_byteswap_ulong(*(uint32_t*)&testData[i][0]) < _byteswap_ulong(*(uint32_t*)&testData[i + 1][0])); }*/ int totalKeyBytes = 0; for (auto& s : testData) totalKeyBytes += s.size(); printf("%d bytes in %lu keys\n", totalKeyBytes, testData.size()); for (int i = 0; i < 5; i++) printf(" '%s'\n", printable(StringRef(testData[i])).c_str()); CompactPreOrderTree* t = (CompactPreOrderTree*)new uint8_t[sizeof(CompactPreOrderTree) + totalKeyBytes + CompactPreOrderTree::Node::getMaxOverhead() * testData.size()]; t1 = timer_monotonic(); int compactTreeBytes = t->build(testData); t2 = timer_monotonic(); printf("Compact tree is %d bytes\n", compactTreeBytes); printf("Build time %0.0f us (%0.2f M/sec)\n", (t2 - t1)*1e6, 1 / (t2 - t1) / 1e6); t1 = timer_monotonic(); const int nBuild = 20000; for (int i = 0; i < nBuild; i++) r += t->build(testData); t2 = timer_monotonic(); printf("Build time %0.0f us (%0.2f M/sec)\n", (t2 - t1)/nBuild*1e6, nBuild / (t2 - t1) / 1e6); PrefixTree *pt = (PrefixTree *)new uint8_t[sizeof(PrefixTree) + totalKeyBytes + testData.size() * PrefixTree::Node::getMaxOverhead(1, 256, 256)]; std::vector keys; for(auto &k : testData) { keys.emplace_back(k, StringRef()); } t1 = timer_monotonic(); int prefixTreeBytes = pt->build(&*keys.begin(), &*keys.end(), StringRef(), StringRef()); t2 = timer_monotonic(); if(!prefixTreeDOTFile.empty()) { FILE *fout = fopen(prefixTreeDOTFile.c_str(), "w"); fprintf(fout, "%s\n", pt->toDOT(StringRef(), StringRef()).c_str()); fclose(fout); } // Calculate perfect prefix-compressed size int perfectSize = testData.front().size(); for(int i = 1; i < testData.size(); ++i) { int common = commonPrefixLength(StringRef(testData[i]), StringRef(testData[i - 1])); perfectSize += (testData[i].size() - common); } printf("PrefixTree tree is %d bytes\n", prefixTreeBytes); printf("Perfect compressed size with no overhead is %d, average PrefixTree overhead is %.2f per item\n", perfectSize, double(prefixTreeBytes - perfectSize) / testData.size()); printf("PrefixTree Build time %0.0f us (%0.2f M/sec)\n", (t2 - t1)*1e6, 1 / (t2 - t1) / 1e6); // Test cursor forward iteration auto c = pt->getCursor(StringRef(), StringRef()); ASSERT(c.moveFirst()); bool end = false; for(int i = 0; i < keys.size(); ++i) { ASSERT(c.getKey() == keys[i].key); end = !c.moveNext(); } ASSERT(end); printf("PrefixTree forward scan passed\n"); // Test cursor backward iteration ASSERT(c.moveLast()); for(int i = keys.size() - 1; i >= 0; --i) { ASSERT(c.getKey() == keys[i].key); end = !c.movePrev(); } ASSERT(end); printf("PrefixTree reverse scan passed\n"); t1 = timer_monotonic(); for (int i = 0; i < nBuild; i++) r += pt->build(&*keys.begin(), &*keys.end(), StringRef(), StringRef()); t2 = timer_monotonic(); printf("PrefixTree Build time %0.0f us (%0.2f M/sec)\n", (t2 - t1)/nBuild*1e6, nBuild / (t2 - t1) / 1e6); t->lastLessOrEqual(LiteralStringRef("8f9fad2e5e2af980a")); { std::string s, s1; CompactPreOrderTree::Node *n; for (int i = 0; i < testData.size(); i++) { s = testData[i]; auto s1 = s;// s.substr(0, s.size() - 1); if (!s1.back()) s1 = s1.substr(0, s1.size() - 1); else { s1.back()--; s1 += "\xff\xff\xff\xff\xff\xff"; } auto n = t->lastLessOrEqual(s1); //printf("lastLessOrEqual(%s) = %s\n", s1.c_str(), n ? n->key().toString().c_str() : "(null)"); ASSERT(i ? testData[i - 1].substr(n->keyPrefixLength()) == n->key() : !n); n = t->lastLessOrEqual(s); //printf("lastLessOrEqual(%s) = %s\n", s.c_str(), n ? n->key().toString().c_str() : "(null)"); ASSERT(n->key() == s.substr(n->keyPrefixLength())); s1 = s + "a"; auto n1 = t->lastLessOrEqual(s1); //printf("lastLessOrEqual(%s) = %s\n", s1.c_str(), n ? n->key().toString().c_str() : "(null)"); ASSERT(n1->key() == s.substr(n1->keyPrefixLength())); ASSERT(CompactPreOrderTree::lastLessOrEqual2(t, t, s, s1) == std::make_pair(n, n1)); } printf("compactMap lastLessOrEqual tests passed\n"); } { auto cur = pt->getCursor(StringRef(), StringRef()); for (int i = 0; i < keys.size(); i++) { StringRef s = keys[i].key; ASSERT(cur.seekLessThanOrEqual(s)); ASSERT(cur.valid()); ASSERT(cur.getKey() == s); StringRef shortString = s.substr(0, s.size() - 1); bool shorter = cur.seekLessThanOrEqual(shortString); if(i > 0) { if(shortString >= keys[i - 1].key) { ASSERT(shorter); ASSERT(cur.valid()); ASSERT(cur.getKey() == keys[i - 1].key); } } else { ASSERT(!shorter); } ASSERT(cur.seekLessThanOrEqual(s.toString() + '\0')); ASSERT(cur.valid()); ASSERT(cur.getKey() == s); } printf("PrefixTree lastLessOrEqual tests passed\n"); } printf("Making %lu copies:\n", 2*sampleQueries.size()); std::vector copies; for (int i = 0; i < 2*sampleQueries.size(); i++) { copies.push_back((CompactPreOrderTree*) new uint8_t[compactTreeBytes]); memcpy(copies.back(), t, compactTreeBytes); } g_random->randomShuffle(copies); std::vector prefixTreeCopies; for (int i = 0; i < 2*sampleQueries.size(); i++) { prefixTreeCopies.push_back((PrefixTree *) new uint8_t[prefixTreeBytes]); memcpy(prefixTreeCopies.back(), pt, prefixTreeBytes); } g_random->randomShuffle(prefixTreeCopies); std::vector> array_copies; for (int i = 0; i < sampleQueries.size(); i++) { array_copies.push_back(testData); } g_random->randomShuffle(array_copies); printf("shuffled\n"); t1 = timer_monotonic(); for (auto& q : sampleQueries) r += (intptr_t)t->lastLessOrEqual(q); t2 = timer_monotonic(); printf("compactmap, in cache: %d queries in %0.3f sec: %0.3f M/sec\n", (int)sampleQueries.size(), t2 - t1, sampleQueries.size() / (t2 - t1) / 1e6); auto cur = pt->getCursor(StringRef(), StringRef()); t1 = timer_monotonic(); for (auto& q : sampleQueries) r += cur.seekLessThanOrEqual(StringRef(q)) ? 1 : 0; t2 = timer_monotonic(); printf("prefixtree, in cache: %d queries in %0.3f sec: %0.3f M/sec\n", (int)sampleQueries.size(), t2 - t1, sampleQueries.size() / (t2 - t1) / 1e6); /* t1 = timer_monotonic(); for (int q = 0; q < sampleQueries.size(); q += 2) { auto x = CompactPreOrderTree::lastLessOrEqual2(t, t, sampleQueries[q], sampleQueries[q + 1]); r += (intptr_t)x.first + (intptr_t)x.second; } t2 = timer_monotonic(); printf("in cache (2x interleaved): %d queries in %0.3f sec: %0.3f M/sec\n", (int)sampleQueries.size(), t2 - t1, sampleQueries.size() / (t2 - t1) / 1e6); */ t1 = timer_monotonic(); for (int q = 0; q < sampleQueries.size(); q++) r += (intptr_t)copies[q]->lastLessOrEqual(sampleQueries[q]); t2 = timer_monotonic(); printf("compactmap, out of cache: %d queries in %0.3f sec: %0.3f M/sec\n", (int)sampleQueries.size(), t2 - t1, sampleQueries.size() / (t2 - t1) / 1e6); std::vector cursors; for (int q = 0; q < sampleQueries.size(); q++) cursors.push_back(prefixTreeCopies[q]->getCursor(StringRef(), StringRef())); t1 = timer_monotonic(); for (int q = 0; q < sampleQueries.size(); q++) r += cursors[q].seekLessThanOrEqual(sampleQueries[q]) ? 1 : 0; t2 = timer_monotonic(); printf("prefixtree, out of cache: %d queries in %0.3f sec: %0.3f M/sec\n", (int)sampleQueries.size(), t2 - t1, sampleQueries.size() / (t2 - t1) / 1e6); /* t1 = timer_monotonic(); for (int q = 0; q < sampleQueries.size(); q += 2) { auto x = CompactPreOrderTree::lastLessOrEqual2(copies[q + sampleQueries.size()], copies[q + sampleQueries.size() + 1], sampleQueries[q], sampleQueries[q + 1]); r += (intptr_t)x.first + (intptr_t)x.second; } t2 = timer_monotonic(); printf("out of cache (2x interleaved): %d queries in %0.3f sec: %0.3f M/sec\n", (int)sampleQueries.size(), t2 - t1, sampleQueries.size() / (t2 - t1) / 1e6); */ t1 = timer_monotonic(); for (int q = 0; q < sampleQueries.size(); q++) r += (intptr_t)(std::lower_bound(array_copies[q].begin(), array_copies[q].end(), sampleQueries[q]) - testData.begin()); t2 = timer_monotonic(); printf("std::lower_bound: %d queries in %0.3f sec: %0.3f M/sec\n", (int)sampleQueries.size(), t2 - t1, sampleQueries.size() / (t2 - t1) / 1e6); } std::vector sampleDocuments(int N) { std::vector testData; std::string p = "pre"; std::string n = "\x01""name\x00\x00"; std::string a = "\x01""address\x00\x00"; std::string o = "\x01""orders\x00\x00"; std::string oi = "\x01""id\x00\x00"; std::string oa = "\x01""amount\x00\x00"; std::string dbl = "\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00"; for (int i = 0; i < N; i++) { std::string id = BinaryWriter::toValue(g_random->randomUniqueID(), Unversioned()).substr(12).toString(); testData.push_back(p + id + n); testData.push_back(p + id + a); for (int j = 0; j < 5; j++) { std::string okey = p + id + o + dbl + (char)j; testData.push_back(okey + oi); testData.push_back(okey + oa); } } return testData; } StringRef shortestKeyBetween(StringRef a, StringRef b) { int p = commonPrefixLength(a.begin(), b.begin(), std::min(a.size(), b.size())); ASSERT(p < b.size()); return b.substr(0, p + 1); } std::vector sampleBPlusTreeSeparators( std::vector rawDocs, int prefixToStrip ) { // In the middle of a B+Tree, we won't have adjacent document keys but separators between // pages. These need only contain as many bytes as necessary to distinguish the last item // in the previous page and the first item in the next page ("suffix compression"), and when // balancing the tree we can move a few keys left or right if it makes a big difference in the // suffix size ("split interval") // The B+Tree will presumably also do its own prefix compression, so we trim off the "obvious" // common prefix for this imaginary middle node std::vector testData; std::sort(rawDocs.begin(), rawDocs.end()); for (int i = 0; i + 1 < rawDocs.size(); i += 1000) { StringRef bestSplitPoint = shortestKeyBetween(rawDocs[i], rawDocs[i + 1]); for (int j = i + 1; j < i + 11; j++) { StringRef s = shortestKeyBetween(rawDocs[j], rawDocs[j + 1]); if (s.size() < bestSplitPoint.size()) bestSplitPoint = s; } testData.push_back(bestSplitPoint.substr(prefixToStrip).toString()); } return testData; } struct Page { Page() : tree(nullptr), size(0), sizeBuilt(0), unsortedKeys(0) {} std::vector keys; PrefixTree *tree; std::string treeBuffer; int size; int sizeBuilt; int unsortedKeys; void add(StringRef k) { keys.emplace_back(k, StringRef()); size += k.size(); ++unsortedKeys; } void sort() { static auto cmp = [=](const PrefixTree::EntryRef &a, const PrefixTree::EntryRef &b) { return a.key < b.key; }; if(unsortedKeys > 0) { // sort newest elements, then merge std::sort(keys.end() - unsortedKeys, keys.end(), cmp); std::inplace_merge(keys.begin(), keys.end() - unsortedKeys, keys.end(), cmp); unsortedKeys = 0; } } int build() { if(sizeBuilt != size) { sort(); treeBuffer.reserve(keys.size() * PrefixTree::Node::getMaxOverhead(1, 256, 256) + size); tree = (PrefixTree *)treeBuffer.data(); int b = tree->build(&*keys.begin(), &*keys.end(), StringRef(), StringRef()); sizeBuilt = size; return b; } return 0; } }; void ingestBenchmark() { std::vector keys_generated; Arena arena; std::set testmap; for(int i = 0; i < 1000000; ++i) { keys_generated.push_back(StringRef(arena, format("........%02X......%02X.....%02X........%02X", g_random->randomInt(0, 100), g_random->randomInt(0, 100), g_random->randomInt(0, 100), g_random->randomInt(0, 100) ))); } double t1 = timer_monotonic(); for(const auto &k : keys_generated) testmap.insert(k); double t2 = timer_monotonic(); printf("Ingested %d elements into map, Speed %f M/s\n", (int)keys_generated.size(), keys_generated.size() / (t2 - t1) / 1e6); // sort a group after k elements were added for(int k = 5; k <= 20; k += 5) { // g is average page delta size for(int g = 10; g <= 150; g += 10) { // rebuild page after r bytes added for(int r = 500; r <= 4000; r += 500) { double elapsed = timer_monotonic(); int builds = 0; int buildbytes = 0; int keybytes = 0; std::vector pages; int pageCount = keys_generated.size() / g; pages.resize(pageCount); for(auto &key : keys_generated) { int p = g_random->randomInt(0, pageCount); Page *&pPage = pages[p]; if(pPage == nullptr) pPage = new Page(); Page &page = *pPage; page.add(key); keybytes += key.size(); if(page.keys.size() % k == 0) { page.sort(); } // Rebuild page after r bytes added if(page.size - page.sizeBuilt > r) { int b = page.build(); if(b > 0) { ++builds; buildbytes += b; } } } for(auto p : pages) { if(p) { int b = p->build(); if(b > 0) { ++builds; buildbytes += b; } } } elapsed = timer_monotonic() - elapsed; printf("%6d keys %6d pages %3f builds/page %6d builds/s %6d pages/s %5d avg keys/page sort every %d deltas rebuild every %5d bytes %7d keys/s %8d keybytes/s\n", (int)keys_generated.size(), pageCount, (double)builds / pageCount, int(builds / elapsed), int(pageCount/elapsed), g, k, r, int(keys_generated.size() / elapsed), int(keybytes / elapsed)); for(auto p : pages) { delete p; } } } } } int main() { printf("CompactMap test\n"); #ifndef NDEBUG printf("Compiler optimization is OFF\n"); #endif printf("Key prefix compression is %s\n", CompactPreOrderTree::Node::ENABLE_PREFIX ? "ON" : "OFF"); printf("Right subtree prefetch is %s\n", CompactPreOrderTree::ENABLE_PREFETCH_RIGHT ? "ON" : "OFF"); printf("Left pointer is %s\n", CompactPreOrderTree::Node::ENABLE_LEFT_PTR ? "ON" : "OFF"); printf("Fancy build is %s\n", CompactPreOrderTree::ENABLE_FANCY_BUILD ? "ON" : "OFF"); g_random = new DeterministicRandom(1); //ingestBenchmark(); /*for (int subtree_size = 1; subtree_size < 20; subtree_size++) { printf("Subtree of size %d:\n", subtree_size); int s = lessOrEqualPowerOfTwo((subtree_size - 1) / 2 + 1) - 1; printf(" s=%d\n", s); printf(" 1 + s + s=%d\n", 1 + s + s); printf(" left: %d\n", subtree_size - 1 - 2 * s); printf(" s*2+1: %d %d\n", s * 2 + 1, subtree_size - (s * 2 + 1) - 1); printf(" n-s-1: %d %d\n", subtree_size-s-1, s); printf(" min: %d %d\n", std::min(s * 2 + 1, subtree_size - s - 1), subtree_size - std::min(s * 2 + 1, subtree_size - s - 1) - 1); }*/ printf("\n16 byte hexadecimal random keys\n"); std::vector testData; for (int i = 0; i < 200; i++) { testData.push_back(g_random->randomUniqueID().shortString()); } std::vector sampleQueries; for (int i = 0; i < 10000; i++) { sampleQueries.push_back(g_random->randomUniqueID().shortString().substr(0, g_random->randomInt(0, 16))); } compactMapTests(testData, sampleQueries); printf("\nRaw index keys\n"); testData.clear(); sampleQueries.clear(); for (int i = 0; i < 100; i++) { testData.push_back(format("%d Main Street #%d, New York NY 12345, United States of America|", 1234 * (i / 100), (i/10) % 10 + 1000) + g_random->randomUniqueID().shortString()); } for (int i = 0; i < 10000; i++) sampleQueries.push_back(format("%d Main Street", g_random->randomInt(1000, 10000))); compactMapTests(testData, sampleQueries, "graph_addresses.dot"); printf("\nb+tree separators for index keys\n"); testData.clear(); for (int i = 0; i < 100000; i++) { testData.push_back(format("%d Main Street #%d, New York NY 12345, United States of America|", 12 * (i / 100), (i/10) % 10 + 1000) + g_random->randomUniqueID().shortString()); } testData = sampleBPlusTreeSeparators(testData, 0); compactMapTests(testData, sampleQueries); printf("\nraw document keys\n"); testData = sampleDocuments(20); sampleQueries.clear(); std::string p = "pre"; for (int i = 0; i < 10000; i++) sampleQueries.push_back(p + BinaryWriter::toValue(g_random->randomUniqueID(), Unversioned()).substr(12).toString()); compactMapTests(testData, sampleQueries); printf("\nb+tree split keys for documents\n"); testData = sampleBPlusTreeSeparators(sampleDocuments(30000), p.size()); compactMapTests(testData, sampleQueries); return 0; }