906 lines
31 KiB
C++
906 lines
31 KiB
C++
/*
|
|
* CompactMap.cpp
|
|
*
|
|
* This source file is part of the FoundationDB open source project
|
|
*
|
|
* Copyright 2013-2022 Apple Inc. and the FoundationDB project authors
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
#pragma intrinsic(memcmp)
|
|
|
|
#include "flow/flow.h"
|
|
#include "flow/DeterministicRandom.h"
|
|
#include "fdbserver/PrefixTree.h"
|
|
#include <stdio.h>
|
|
|
|
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 ? al < bl : searchKey[np + prefixLen] < n->keyData()[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<Node*, Node*> 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 ? al1 < bl1 : searchKey1[np1 + prefixLen1] < n1->keyData()[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 ? al2 < bl2 : searchKey2[np2 + prefixLen2] < n2->keyData()[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 ? al1 < bl1 : searchKey1[np1 + prefixLen1] < n1->keyData()[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 ? al2 < bl2 : searchKey2[np2 + prefixLen2] < n2->keyData()[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<std::string>& input, std::string const& prefix = std::string()) {
|
|
nodeCount = input.size();
|
|
|
|
Deque< BuildInfo > queue;
|
|
Deque< BuildInfo > deferred;
|
|
queue.push_back(BuildInfo(nullptr, 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<std::string>& 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
|
|
};
|
|
|
|
void compactMapTests(std::vector<std::string> testData,
|
|
std::vector<std::string> 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<PrefixTree::EntryRef> 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.getKeyRef() == 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.getKeyRef() == 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<CompactPreOrderTree*> copies;
|
|
for (int i = 0; i < 2 * sampleQueries.size(); i++) {
|
|
copies.push_back((CompactPreOrderTree*)new uint8_t[compactTreeBytes]);
|
|
memcpy(copies.back(), t, compactTreeBytes);
|
|
}
|
|
deterministicRandom()->randomShuffle(copies);
|
|
|
|
std::vector<PrefixTree*> prefixTreeCopies;
|
|
for (int i = 0; i < 2 * sampleQueries.size(); i++) {
|
|
prefixTreeCopies.push_back((PrefixTree*)new uint8_t[prefixTreeBytes]);
|
|
memcpy(prefixTreeCopies.back(), pt, prefixTreeBytes);
|
|
}
|
|
deterministicRandom()->randomShuffle(prefixTreeCopies);
|
|
|
|
std::vector<std::vector<std::string>> array_copies;
|
|
for (int i = 0; i < sampleQueries.size(); i++) {
|
|
array_copies.push_back(testData);
|
|
}
|
|
deterministicRandom()->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<PrefixTree::Cursor> 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<std::string> sampleDocuments(int N) {
|
|
std::vector<std::string> 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(deterministicRandom()->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<std::string> sampleBPlusTreeSeparators(std::vector<std::string> 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<std::string> 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<PrefixTree::EntryRef> 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<StringRef> keys_generated;
|
|
Arena arena;
|
|
std::set<StringRef> testmap;
|
|
for (int i = 0; i < 1000000; ++i) {
|
|
keys_generated.push_back(StringRef(arena,
|
|
format("........%02X......%02X.....%02X........%02X",
|
|
deterministicRandom()->randomInt(0, 100),
|
|
deterministicRandom()->randomInt(0, 100),
|
|
deterministicRandom()->randomInt(0, 100),
|
|
deterministicRandom()->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<Page*> pages;
|
|
int pageCount = keys_generated.size() / g;
|
|
pages.resize(pageCount);
|
|
|
|
for (auto& key : keys_generated) {
|
|
int p = deterministicRandom()->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");
|
|
|
|
setThreadLocalDeterministicRandomSeed(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<std::string> testData;
|
|
for (int i = 0; i < 200; i++) {
|
|
testData.push_back(deterministicRandom()->randomUniqueID().shortString());
|
|
}
|
|
std::vector<std::string> sampleQueries;
|
|
for (int i = 0; i < 10000; i++) {
|
|
sampleQueries.push_back(
|
|
deterministicRandom()->randomUniqueID().shortString().substr(0, deterministicRandom()->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) +
|
|
deterministicRandom()->randomUniqueID().shortString());
|
|
}
|
|
for (int i = 0; i < 10000; i++)
|
|
sampleQueries.push_back(format("%d Main Street", deterministicRandom()->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) +
|
|
deterministicRandom()->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(deterministicRandom()->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;
|
|
}
|