Compare commits
210 Commits
9a47f2d03a
...
v0.0.1
Author | SHA1 | Date | |
---|---|---|---|
71c39f9955 | |||
8cc17158fd | |||
ab211c646a | |||
7af961f141 | |||
a91df62608 | |||
0a1843a161 | |||
4edf0315d9 | |||
14515e186a | |||
b0085df5ad | |||
76a7e17b29 | |||
5cf43d1bfa | |||
25cc427ec5 | |||
c15c2e7b44 | |||
a4d1f91670 | |||
b7cdecaf71 | |||
cda28643a6 | |||
cdb5360b9a | |||
ef224a60f4 | |||
6222b74787 | |||
19edc6f78f | |||
3f9d01c46a | |||
db03c6f901 | |||
c1698b040b | |||
2e08b54785 | |||
aa6f237d50 | |||
becfd25139 | |||
d78b36821b | |||
ce79b47fbe | |||
727b7e642a | |||
cb4c2b7e1e | |||
ef9b789745 | |||
edd7bcaa1e | |||
be8ac879c5 | |||
83c7f66d67 | |||
a5710b8282 | |||
c31eebd5de | |||
ddeb059968 | |||
5a0bcf9a5a | |||
97717cec86 | |||
6a13c43a78 | |||
c6c438bae2 | |||
7d4f832b43 | |||
5b0c3c2428 | |||
f2b5e9b0bf | |||
8e0e65dac6 | |||
5aab76847a | |||
1a51aa00e5 | |||
3975bada0c | |||
a5330b6e23 | |||
2e246ec6a4 | |||
6d7e3c9849 | |||
671da5d096 | |||
303b368fc5 | |||
9f5a68e2c0 | |||
dfbb3ce5f1 | |||
e7719b6e0b | |||
83fedf1f9e | |||
8556caf360 | |||
9d13ca84f5 | |||
a79436ee9b | |||
e9c8537cf2 | |||
5b988efe6f | |||
e35d698b21 | |||
30496d14e7 | |||
eb93157ddf | |||
9cafef8bbb | |||
6f81580953 | |||
429fe5baed | |||
a0451e4423 | |||
a9b3d3d1c9 | |||
b817e3c749 | |||
ee36bda8f8 | |||
a8f4bd91c8 | |||
35086ee66a | |||
0f795cf163 | |||
a07c93ffff | |||
c68f563017 | |||
6b6a9bace9 | |||
3cb0765fdd | |||
351ff3df3b | |||
e818648cdc | |||
12540b8713 | |||
c2606cd26a | |||
4b72fc0b7b | |||
a9caa0249e | |||
08b2b7f41a | |||
26bd8b94cc | |||
55eaef5b1d | |||
797e6b4a3e | |||
ee86b5289b | |||
b779c0f6f7 | |||
ef802b8acd | |||
5371c2bede | |||
75c304bbe7 | |||
aefb83dbc6 | |||
b0ac7e41b9 | |||
4b6b2747bf | |||
1496aa106b | |||
71e117965e | |||
471b276947 | |||
b721bc80a9 | |||
5e4eab55fb | |||
1dcb380c73 | |||
87d650ff00 | |||
b8f6a8edf2 | |||
01f1d5850f | |||
cd567383c3 | |||
53a442abf9 | |||
6e212847ac | |||
44a023c2f4 | |||
e32bea7b29 | |||
504a93bb10 | |||
b79d8f71d3 | |||
34430dbbe7 | |||
06fcb2531e | |||
bd24a362e3 | |||
1437280ec7 | |||
e5051bac9e | |||
733f32b22e | |||
3fb8bf7c3b | |||
0c8cb8faa5 | |||
93e487c8fb | |||
d91538dcad | |||
43a768d152 | |||
2989866a6d | |||
60df97847c | |||
0038382661 | |||
782abc70d6 | |||
8802d17acd | |||
02afd47d8f | |||
987e93b190 | |||
81263f5abf | |||
2689901637 | |||
87dd70c4b6 | |||
451ac5b2b6 | |||
a8042ab20d | |||
8a36e72640 | |||
1519216d08 | |||
f2cd05c29d | |||
5e1fb1dac5 | |||
d1a6b293e9 | |||
be43143891 | |||
53bc36f628 | |||
0f360fa806 | |||
00389936a8 | |||
04f75d57e9 | |||
6a0344e821 | |||
2fcf3da29f | |||
c8495b1695 | |||
d81d02f11d | |||
be5f1b67c8 | |||
ec3aec4dff | |||
9a4eed9453 | |||
30abf7833d | |||
d9c0d24e58 | |||
3f121dc681 | |||
3ea0f405f2 | |||
1811342cb6 | |||
16aa52c071 | |||
b68e04ba80 | |||
45e8d68234 | |||
760a99098a | |||
e6a88852b3 | |||
b97f611a3c | |||
10436096d1 | |||
ad11782029 | |||
8bf3aa7f56 | |||
a4b03bc216 | |||
946694b8a5 | |||
7345eceab3 | |||
9b50393e15 | |||
6c8655798a | |||
86e99e4664 | |||
6b99b85f9e | |||
92fea7f56b | |||
717264b452 | |||
68bd39e130 | |||
25a1226667 | |||
d4d2dbcbda | |||
6d9b35396f | |||
0c177fb40f | |||
f38198a39d | |||
5f7789128e | |||
3f45535f89 | |||
2b0dbabb5c | |||
be7f643f14 | |||
6a08bdd40e | |||
333ac74d91 | |||
17ac9b38fb | |||
172dd40648 | |||
c97c7eee8e | |||
9fcfc44dc3 | |||
0d3475e229 | |||
70e3377eac | |||
adaa652d0d | |||
921005edb3 | |||
d43a8a5907 | |||
588e8eb87f | |||
df8d092a84 | |||
438a2e2abc | |||
305c218888 | |||
db60782c48 | |||
4f32ecc26e | |||
f84aa88202 | |||
921da1cb3f | |||
14de4ee297 | |||
7136b5a450 | |||
116c79d3de | |||
aaf0283f66 | |||
259f47664a |
2
.clangd
2
.clangd
@@ -1,2 +1,2 @@
|
||||
CompileFlags:
|
||||
Add: [-DENABLE_MAIN, -UNDEBUG, -DENABLE_FUZZ, -fexceptions]
|
||||
Add: [-DENABLE_MAIN, -UNDEBUG, -DENABLE_FUZZ, -DTHREAD_TEST, -fexceptions]
|
||||
|
@@ -15,4 +15,12 @@ repos:
|
||||
description: disallow checking in DEBUG_VERBOSE=1
|
||||
entry: '^#define DEBUG_VERBOSE 1$'
|
||||
language: pygrep
|
||||
types: [c++]
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: debug verbose check
|
||||
name: disallow checking in SHOW_MEMORY=1
|
||||
description: disallow checking in SHOW_MEMORY=1
|
||||
entry: '^#define SHOW_MEMORY 1$'
|
||||
language: pygrep
|
||||
types: [c++]
|
9
.vscode/settings.json
vendored
Normal file
9
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"files.associations": {
|
||||
"*.tikz": "latex"
|
||||
},
|
||||
"latex-workshop.view.pdf.invertMode.enabled": "compat",
|
||||
"latex-workshop.view.pdf.invert": 1,
|
||||
"latex-workshop.view.pdf.invertMode.sepia": 1,
|
||||
"latex-workshop.view.pdf.invertMode.grayscale": 0.5
|
||||
}
|
762
Bench.cpp
762
Bench.cpp
@@ -2,641 +2,14 @@
|
||||
#include "Internal.h"
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
#if SHOW_MEMORY
|
||||
void showMemory(const ConflictSet &cs);
|
||||
#endif
|
||||
|
||||
#define ANKERL_NANOBENCH_IMPLEMENT
|
||||
#include "third_party/nanobench.h"
|
||||
|
||||
std::span<const uint8_t> keyAfter(Arena &arena, std::span<const uint8_t> key) {
|
||||
auto result =
|
||||
std::span<uint8_t>(new (arena) uint8_t[key.size() + 1], key.size() + 1);
|
||||
memcpy(result.data(), key.data(), key.size());
|
||||
result[result.size() - 1] = 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
using Version = int64_t;
|
||||
#define force_inline __attribute__((always_inline))
|
||||
using StringRef = std::span<const uint8_t>;
|
||||
|
||||
struct KeyRangeRef {
|
||||
StringRef begin;
|
||||
StringRef end;
|
||||
KeyRangeRef() {}
|
||||
KeyRangeRef(StringRef begin, StringRef end) : begin(begin), end(end) {}
|
||||
KeyRangeRef(Arena &arena, StringRef begin)
|
||||
: begin(begin), end(keyAfter(arena, begin)) {}
|
||||
};
|
||||
|
||||
static thread_local uint32_t g_seed = 0;
|
||||
|
||||
static inline int skfastrand() {
|
||||
g_seed = g_seed * 1664525L + 1013904223L;
|
||||
return g_seed;
|
||||
}
|
||||
|
||||
static int compare(const StringRef &a, const StringRef &b) {
|
||||
int c = memcmp(a.data(), b.data(), std::min(a.size(), b.size()));
|
||||
if (c < 0)
|
||||
return -1;
|
||||
if (c > 0)
|
||||
return +1;
|
||||
if (a.size() < b.size())
|
||||
return -1;
|
||||
if (a.size() == b.size())
|
||||
return 0;
|
||||
return +1;
|
||||
}
|
||||
|
||||
struct ReadConflictRange {
|
||||
StringRef begin, end;
|
||||
Version version;
|
||||
|
||||
ReadConflictRange() {}
|
||||
ReadConflictRange(StringRef begin, StringRef end, Version version)
|
||||
: begin(begin), end(end), version(version) {}
|
||||
bool operator<(const ReadConflictRange &rhs) const {
|
||||
return compare(begin, rhs.begin) < 0;
|
||||
}
|
||||
};
|
||||
|
||||
class SkipList {
|
||||
private:
|
||||
static constexpr int MaxLevels = 26;
|
||||
|
||||
int randomLevel() const {
|
||||
uint32_t i = uint32_t(skfastrand()) >> (32 - (MaxLevels - 1));
|
||||
int level = 0;
|
||||
while (i & 1) {
|
||||
i >>= 1;
|
||||
level++;
|
||||
}
|
||||
assert(level < MaxLevels);
|
||||
return level;
|
||||
}
|
||||
|
||||
// Represent a node in the SkipList. The node has multiple (i.e., level)
|
||||
// pointers to other nodes, and keeps a record of the max versions for each
|
||||
// level.
|
||||
struct Node {
|
||||
int level() const { return nPointers - 1; }
|
||||
uint8_t *value() {
|
||||
return end() + nPointers * (sizeof(Node *) + sizeof(Version));
|
||||
}
|
||||
int length() const { return valueLength; }
|
||||
|
||||
// Returns the next node pointer at the given level.
|
||||
Node *getNext(int level) { return *((Node **)end() + level); }
|
||||
// Sets the next node pointer at the given level.
|
||||
void setNext(int level, Node *n) { *((Node **)end() + level) = n; }
|
||||
|
||||
// Returns the max version at the given level.
|
||||
Version getMaxVersion(int i) const {
|
||||
return ((Version *)(end() + nPointers * sizeof(Node *)))[i];
|
||||
}
|
||||
// Sets the max version at the given level.
|
||||
void setMaxVersion(int i, Version v) {
|
||||
((Version *)(end() + nPointers * sizeof(Node *)))[i] = v;
|
||||
}
|
||||
|
||||
// Return a node with initialized value but uninitialized pointers
|
||||
// Memory layout: *this, (level+1) Node*, (level+1) Version, value
|
||||
static Node *create(const StringRef &value, int level) {
|
||||
int nodeSize = sizeof(Node) + value.size() +
|
||||
(level + 1) * (sizeof(Node *) + sizeof(Version));
|
||||
|
||||
Node *n;
|
||||
n = (Node *)new char[nodeSize];
|
||||
|
||||
n->nPointers = level + 1;
|
||||
|
||||
n->valueLength = value.size();
|
||||
if (value.size() > 0) {
|
||||
memcpy(n->value(), value.data(), value.size());
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
// pre: level>0, all lower level nodes between this and getNext(level) have
|
||||
// correct maxversions
|
||||
void calcVersionForLevel(int level) {
|
||||
Node *end = getNext(level);
|
||||
Version v = getMaxVersion(level - 1);
|
||||
for (Node *x = getNext(level - 1); x != end; x = x->getNext(level - 1))
|
||||
v = std::max(v, x->getMaxVersion(level - 1));
|
||||
setMaxVersion(level, v);
|
||||
}
|
||||
|
||||
void destroy() { delete[](char *) this; }
|
||||
|
||||
private:
|
||||
int getNodeSize() const {
|
||||
return sizeof(Node) + valueLength +
|
||||
nPointers * (sizeof(Node *) + sizeof(Version));
|
||||
}
|
||||
// Returns the first Node* pointer
|
||||
uint8_t *end() { return (uint8_t *)(this + 1); }
|
||||
uint8_t const *end() const { return (uint8_t const *)(this + 1); }
|
||||
int nPointers, valueLength;
|
||||
};
|
||||
|
||||
static force_inline bool less(const uint8_t *a, int aLen, const uint8_t *b,
|
||||
int bLen) {
|
||||
int c = memcmp(a, b, std::min(aLen, bLen));
|
||||
if (c < 0)
|
||||
return true;
|
||||
if (c > 0)
|
||||
return false;
|
||||
return aLen < bLen;
|
||||
}
|
||||
|
||||
Node *header;
|
||||
|
||||
void destroy() {
|
||||
Node *next, *x;
|
||||
for (x = header; x; x = next) {
|
||||
next = x->getNext(0);
|
||||
x->destroy();
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
// Points the location (i.e., Node*) that value would appear in the SkipList.
|
||||
// If the "value" is in the list, then finger[0] points to that exact node;
|
||||
// otherwise, the finger points to Nodes that the value should be inserted
|
||||
// before. Note the SkipList organizes all nodes at level 0, higher levels
|
||||
// contain jump pointers.
|
||||
struct Finger {
|
||||
Node *finger[MaxLevels]; // valid for levels >= level
|
||||
int level = MaxLevels;
|
||||
Node *x = nullptr;
|
||||
Node *alreadyChecked = nullptr;
|
||||
StringRef value;
|
||||
|
||||
Finger() = default;
|
||||
Finger(Node *header, const StringRef &ptr) : x(header), value(ptr) {}
|
||||
|
||||
void init(const StringRef &value, Node *header) {
|
||||
this->value = value;
|
||||
x = header;
|
||||
alreadyChecked = nullptr;
|
||||
level = MaxLevels;
|
||||
}
|
||||
|
||||
// pre: !finished()
|
||||
force_inline void prefetch() {
|
||||
Node *next = x->getNext(0);
|
||||
__builtin_prefetch(next);
|
||||
}
|
||||
|
||||
// pre: !finished()
|
||||
// Advances the pointer at the current level to a Node that's >= finger's
|
||||
// value if possible; or move to the next level (i.e., level--). Returns
|
||||
// true if we have advanced to the next level
|
||||
force_inline bool advance() {
|
||||
Node *next = x->getNext(level - 1);
|
||||
|
||||
if (next == alreadyChecked ||
|
||||
!less(next->value(), next->length(), value.data(), value.size())) {
|
||||
alreadyChecked = next;
|
||||
level--;
|
||||
finger[level] = x;
|
||||
return true;
|
||||
} else {
|
||||
x = next;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// pre: !finished()
|
||||
force_inline void nextLevel() {
|
||||
while (!advance())
|
||||
;
|
||||
}
|
||||
|
||||
force_inline bool finished() const { return level == 0; }
|
||||
|
||||
// Returns if the finger value is found in the SkipList.
|
||||
force_inline Node *found() const {
|
||||
// valid after finished returns true
|
||||
Node *n = finger[0]->getNext(
|
||||
0); // or alreadyChecked, but that is more easily invalidated
|
||||
if (n && n->length() == value.size() &&
|
||||
!memcmp(n->value(), value.data(), value.size()))
|
||||
return n;
|
||||
else
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
StringRef getValue() const {
|
||||
Node *n = finger[0]->getNext(0);
|
||||
return n ? StringRef(n->value(), n->length()) : StringRef();
|
||||
}
|
||||
};
|
||||
|
||||
// Returns the total number of nodes in the list.
|
||||
int count() const {
|
||||
int count = 0;
|
||||
Node *x = header->getNext(0);
|
||||
while (x) {
|
||||
x = x->getNext(0);
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
explicit SkipList(Version version = 0) {
|
||||
header = Node::create(StringRef(), MaxLevels - 1);
|
||||
for (int l = 0; l < MaxLevels; l++) {
|
||||
header->setNext(l, nullptr);
|
||||
header->setMaxVersion(l, version);
|
||||
}
|
||||
}
|
||||
~SkipList() { destroy(); }
|
||||
SkipList(SkipList &&other) noexcept : header(other.header) {
|
||||
other.header = nullptr;
|
||||
}
|
||||
void operator=(SkipList &&other) noexcept {
|
||||
destroy();
|
||||
header = other.header;
|
||||
other.header = nullptr;
|
||||
}
|
||||
void swap(SkipList &other) { std::swap(header, other.header); }
|
||||
|
||||
void addConflictRanges(const Finger *fingers, int rangeCount,
|
||||
Version *version) {
|
||||
for (int r = rangeCount - 1; r >= 0; r--) {
|
||||
const Finger &startF = fingers[r * 2];
|
||||
const Finger &endF = fingers[r * 2 + 1];
|
||||
|
||||
if (endF.found() == nullptr)
|
||||
insert(endF, endF.finger[0]->getMaxVersion(0));
|
||||
|
||||
remove(startF, endF);
|
||||
insert(startF, version[r]);
|
||||
}
|
||||
}
|
||||
|
||||
void detectConflicts(ReadConflictRange *ranges, int count,
|
||||
ConflictSet::Result *transactionConflictStatus) const {
|
||||
const int M = 16;
|
||||
int nextJob[M];
|
||||
CheckMax inProgress[M];
|
||||
if (!count)
|
||||
return;
|
||||
|
||||
int started = std::min(M, count);
|
||||
for (int i = 0; i < started; i++) {
|
||||
inProgress[i].init(ranges[i], header, transactionConflictStatus + i);
|
||||
nextJob[i] = i + 1;
|
||||
}
|
||||
nextJob[started - 1] = 0;
|
||||
|
||||
int prevJob = started - 1;
|
||||
int job = 0;
|
||||
// vtune: 340 parts
|
||||
while (true) {
|
||||
if (inProgress[job].advance()) {
|
||||
if (started == count) {
|
||||
if (prevJob == job)
|
||||
break;
|
||||
nextJob[prevJob] = nextJob[job];
|
||||
job = prevJob;
|
||||
} else {
|
||||
int temp = started++;
|
||||
inProgress[job].init(ranges[temp], header,
|
||||
transactionConflictStatus + temp);
|
||||
}
|
||||
}
|
||||
prevJob = job;
|
||||
job = nextJob[job];
|
||||
}
|
||||
}
|
||||
|
||||
void find(const StringRef *values, Finger *results, int *temp, int count) {
|
||||
// Relying on the ordering of values, descend until the values aren't all in
|
||||
// the same part of the tree
|
||||
|
||||
// vtune: 11 parts
|
||||
results[0].init(values[0], header);
|
||||
const StringRef &endValue = values[count - 1];
|
||||
while (results[0].level > 1) {
|
||||
results[0].nextLevel();
|
||||
Node *ac = results[0].alreadyChecked;
|
||||
if (ac &&
|
||||
less(ac->value(), ac->length(), endValue.data(), endValue.size()))
|
||||
break;
|
||||
}
|
||||
|
||||
// Init all the other fingers to start descending where we stopped
|
||||
// the first one
|
||||
|
||||
// SOMEDAY: this loop showed up on vtune, could be faster?
|
||||
// vtune: 8 parts
|
||||
int startLevel = results[0].level + 1;
|
||||
Node *x = startLevel < MaxLevels ? results[0].finger[startLevel] : header;
|
||||
for (int i = 1; i < count; i++) {
|
||||
results[i].level = startLevel;
|
||||
results[i].x = x;
|
||||
results[i].alreadyChecked = nullptr;
|
||||
results[i].value = values[i];
|
||||
for (int j = startLevel; j < MaxLevels; j++)
|
||||
results[i].finger[j] = results[0].finger[j];
|
||||
}
|
||||
|
||||
int *nextJob = temp;
|
||||
for (int i = 0; i < count - 1; i++)
|
||||
nextJob[i] = i + 1;
|
||||
nextJob[count - 1] = 0;
|
||||
|
||||
int prevJob = count - 1;
|
||||
int job = 0;
|
||||
|
||||
// vtune: 225 parts
|
||||
while (true) {
|
||||
Finger *f = &results[job];
|
||||
f->advance();
|
||||
if (f->finished()) {
|
||||
if (prevJob == job)
|
||||
break;
|
||||
nextJob[prevJob] = nextJob[job];
|
||||
} else {
|
||||
f->prefetch();
|
||||
prevJob = job;
|
||||
}
|
||||
job = nextJob[job];
|
||||
}
|
||||
}
|
||||
|
||||
int removeBefore(Version v, Finger &f, int nodeCount) {
|
||||
// f.x, f.alreadyChecked?
|
||||
|
||||
int removedCount = 0;
|
||||
bool wasAbove = true;
|
||||
while (nodeCount--) {
|
||||
Node *x = f.finger[0]->getNext(0);
|
||||
if (!x)
|
||||
break;
|
||||
|
||||
// double prefetch gives +25% speed (single threaded)
|
||||
Node *next = x->getNext(0);
|
||||
__builtin_prefetch(next);
|
||||
next = x->getNext(1);
|
||||
__builtin_prefetch(next);
|
||||
|
||||
bool isAbove = x->getMaxVersion(0) >= v;
|
||||
if (isAbove || wasAbove) { // f.nextItem
|
||||
for (int l = 0; l <= x->level(); l++)
|
||||
f.finger[l] = x;
|
||||
} else { // f.eraseItem
|
||||
removedCount++;
|
||||
for (int l = 0; l <= x->level(); l++)
|
||||
f.finger[l]->setNext(l, x->getNext(l));
|
||||
for (int i = 1; i <= x->level(); i++)
|
||||
f.finger[i]->setMaxVersion(
|
||||
i, std::max(f.finger[i]->getMaxVersion(i), x->getMaxVersion(i)));
|
||||
x->destroy();
|
||||
}
|
||||
wasAbove = isAbove;
|
||||
}
|
||||
|
||||
return removedCount;
|
||||
}
|
||||
|
||||
private:
|
||||
void remove(const Finger &start, const Finger &end) {
|
||||
if (start.finger[0] == end.finger[0])
|
||||
return;
|
||||
|
||||
Node *x = start.finger[0]->getNext(0);
|
||||
|
||||
// vtune says: this loop is the expensive parts (6 parts)
|
||||
for (int i = 0; i < MaxLevels; i++)
|
||||
if (start.finger[i] != end.finger[i])
|
||||
start.finger[i]->setNext(i, end.finger[i]->getNext(i));
|
||||
|
||||
while (true) {
|
||||
Node *next = x->getNext(0);
|
||||
x->destroy();
|
||||
if (x == end.finger[0])
|
||||
break;
|
||||
x = next;
|
||||
}
|
||||
}
|
||||
|
||||
void insert(const Finger &f, Version version) {
|
||||
int level = randomLevel();
|
||||
// std::cout << std::string((const char*)value,length) << " level: " <<
|
||||
// level << std::endl;
|
||||
Node *x = Node::create(f.value, level);
|
||||
x->setMaxVersion(0, version);
|
||||
for (int i = 0; i <= level; i++) {
|
||||
x->setNext(i, f.finger[i]->getNext(i));
|
||||
f.finger[i]->setNext(i, x);
|
||||
}
|
||||
// vtune says: this loop is the costly part of this function
|
||||
for (int i = 1; i <= level; i++) {
|
||||
f.finger[i]->calcVersionForLevel(i);
|
||||
x->calcVersionForLevel(i);
|
||||
}
|
||||
for (int i = level + 1; i < MaxLevels; i++) {
|
||||
Version v = f.finger[i]->getMaxVersion(i);
|
||||
if (v >= version)
|
||||
break;
|
||||
f.finger[i]->setMaxVersion(i, version);
|
||||
}
|
||||
}
|
||||
|
||||
struct CheckMax {
|
||||
Finger start, end;
|
||||
Version version;
|
||||
ConflictSet::Result *result;
|
||||
int state;
|
||||
|
||||
void init(const ReadConflictRange &r, Node *header,
|
||||
ConflictSet::Result *result) {
|
||||
this->start.init(r.begin, header);
|
||||
this->end.init(r.end, header);
|
||||
this->version = r.version;
|
||||
this->state = 0;
|
||||
this->result = result;
|
||||
}
|
||||
|
||||
bool noConflict() const { return true; }
|
||||
bool conflict() {
|
||||
*result = ConflictSet::Conflict;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Return true if finished
|
||||
force_inline bool advance() {
|
||||
if (*result == ConflictSet::TooOld) {
|
||||
return true;
|
||||
}
|
||||
switch (state) {
|
||||
case 0:
|
||||
// find where start and end fingers diverge
|
||||
while (true) {
|
||||
if (!start.advance()) {
|
||||
start.prefetch();
|
||||
return false;
|
||||
}
|
||||
end.x = start.x;
|
||||
while (!end.advance())
|
||||
;
|
||||
|
||||
int l = start.level;
|
||||
if (start.finger[l] != end.finger[l])
|
||||
break;
|
||||
// accept if the range spans the check range, but does not have a
|
||||
// greater version
|
||||
if (start.finger[l]->getMaxVersion(l) <= version)
|
||||
return noConflict();
|
||||
if (l == 0)
|
||||
return conflict();
|
||||
}
|
||||
state = 1;
|
||||
case 1: {
|
||||
// check the end side of the pyramid
|
||||
Node *e = end.finger[end.level];
|
||||
while (e->getMaxVersion(end.level) > version) {
|
||||
if (end.finished())
|
||||
return conflict();
|
||||
end.nextLevel();
|
||||
Node *f = end.finger[end.level];
|
||||
while (e != f) {
|
||||
if (e->getMaxVersion(end.level) > version)
|
||||
return conflict();
|
||||
e = e->getNext(end.level);
|
||||
}
|
||||
}
|
||||
|
||||
// check the start side of the pyramid
|
||||
Node *s = end.finger[start.level];
|
||||
while (true) {
|
||||
Node *nextS = start.finger[start.level]->getNext(start.level);
|
||||
Node *p = nextS;
|
||||
while (p != s) {
|
||||
if (p->getMaxVersion(start.level) > version)
|
||||
return conflict();
|
||||
p = p->getNext(start.level);
|
||||
}
|
||||
if (start.finger[start.level]->getMaxVersion(start.level) <= version)
|
||||
return noConflict();
|
||||
s = nextS;
|
||||
if (start.finished()) {
|
||||
if (nextS->length() == start.value.size() &&
|
||||
!memcmp(nextS->value(), start.value.data(), start.value.size()))
|
||||
return noConflict();
|
||||
else
|
||||
return conflict();
|
||||
}
|
||||
start.nextLevel();
|
||||
}
|
||||
}
|
||||
default:
|
||||
__builtin_unreachable();
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
struct SkipListConflictSet {
|
||||
SkipListConflictSet(int64_t oldestVersion)
|
||||
: oldestVersion(oldestVersion), skipList(oldestVersion) {}
|
||||
void check(const ConflictSet::ReadRange *reads, ConflictSet::Result *results,
|
||||
int count) const {
|
||||
Arena arena;
|
||||
auto *ranges = new (arena) ReadConflictRange[count];
|
||||
for (int i = 0; i < count; ++i) {
|
||||
ranges[i].begin = {reads[i].begin.p, size_t(reads[i].begin.len)};
|
||||
ranges[i].end = {reads[i].end.p,
|
||||
size_t(reads[i].end.len == 0 ? reads[i].begin.len + 1
|
||||
: reads[i].end.len)};
|
||||
ranges[i].version = reads[i].readVersion;
|
||||
if (reads[i].readVersion < oldestVersion) {
|
||||
results[i] = ConflictSet::TooOld;
|
||||
} else {
|
||||
results[i] = ConflictSet::Commit;
|
||||
}
|
||||
}
|
||||
skipList.detectConflicts(ranges, count, results);
|
||||
}
|
||||
|
||||
void addWrites(const ConflictSet::WriteRange *writes, int count) {
|
||||
Arena arena;
|
||||
const int stringCount = count * 2;
|
||||
|
||||
const int stripeSize = 16;
|
||||
SkipList::Finger fingers[stripeSize];
|
||||
int temp[stripeSize];
|
||||
int stripes = (stringCount + stripeSize - 1) / stripeSize;
|
||||
StringRef values[stripeSize];
|
||||
int64_t writeVersions[stripeSize / 2];
|
||||
int ss = stringCount - (stripes - 1) * stripeSize;
|
||||
for (int s = stripes - 1; s >= 0; s--) {
|
||||
for (int i = 0; i * 2 < ss; ++i) {
|
||||
const auto &w = writes[s * stripeSize / 2 + i];
|
||||
values[i * 2] = {w.begin.p, size_t(w.begin.len)};
|
||||
values[i * 2 + 1] = {
|
||||
w.end.p, size_t(w.end.len == 0 ? w.begin.len + 1 : w.end.len)};
|
||||
writeVersions[i] = w.writeVersion;
|
||||
keyUpdates += 2;
|
||||
}
|
||||
skipList.find(values, fingers, temp, ss);
|
||||
skipList.addConflictRanges(fingers, ss / 2, writeVersions);
|
||||
ss = stripeSize;
|
||||
}
|
||||
}
|
||||
|
||||
void setOldestVersion(int64_t oldestVersion) {
|
||||
this->oldestVersion = oldestVersion;
|
||||
SkipList::Finger finger;
|
||||
int temp;
|
||||
std::span<const uint8_t> key = removalKey;
|
||||
skipList.find(&key, &finger, &temp, 1);
|
||||
skipList.removeBefore(oldestVersion, finger, std::exchange(keyUpdates, 0));
|
||||
removalKey = std::basic_string<uint8_t>(finger.getValue().data(),
|
||||
finger.getValue().size());
|
||||
}
|
||||
|
||||
private:
|
||||
int64_t keyUpdates = 0;
|
||||
std::basic_string<uint8_t> removalKey;
|
||||
int64_t oldestVersion;
|
||||
SkipList skipList;
|
||||
};
|
||||
|
||||
ConflictSet::ReadRange singleton(Arena &arena, std::span<const uint8_t> key) {
|
||||
auto r =
|
||||
std::span<uint8_t>(new (arena) uint8_t[key.size() + 1], key.size() + 1);
|
||||
memcpy(r.data(), key.data(), key.size());
|
||||
r[key.size()] = 0;
|
||||
return {key.data(), int(key.size()), r.data(), int(r.size())};
|
||||
}
|
||||
|
||||
ConflictSet::ReadRange prefixRange(Arena &arena, std::span<const uint8_t> key) {
|
||||
int index;
|
||||
for (index = key.size() - 1; index >= 0; index--)
|
||||
if ((key[index]) != 255)
|
||||
break;
|
||||
|
||||
// Must not be called with a string that consists only of zero or more '\xff'
|
||||
// bytes.
|
||||
if (index < 0) {
|
||||
assert(false);
|
||||
}
|
||||
|
||||
auto r = std::span<uint8_t>(new (arena) uint8_t[index + 1], index + 1);
|
||||
memcpy(r.data(), key.data(), index + 1);
|
||||
r[r.size() - 1]++;
|
||||
return {key.data(), int(key.size()), r.data(), int(r.size())};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
constexpr int kNumKeys = 1000000;
|
||||
|
||||
constexpr int kOpsPerTx = 100;
|
||||
@@ -656,12 +29,37 @@ std::span<const uint8_t> makeKey(Arena &arena, int index) {
|
||||
return result;
|
||||
}
|
||||
|
||||
template <class ConflictSet_> void benchConflictSet(const std::string &name) {
|
||||
ConflictSet::ReadRange singleton(Arena &arena, std::span<const uint8_t> key) {
|
||||
auto r =
|
||||
std::span<uint8_t>(new (arena) uint8_t[key.size() + 1], key.size() + 1);
|
||||
memcpy(r.data(), key.data(), key.size());
|
||||
r[key.size()] = 0;
|
||||
return {{key.data(), int(key.size())}, {r.data(), int(r.size())}, 0};
|
||||
}
|
||||
|
||||
ConflictSet::ReadRange prefixRange(Arena &arena, std::span<const uint8_t> key) {
|
||||
int index;
|
||||
for (index = key.size() - 1; index >= 0; index--)
|
||||
if ((key[index]) != 255)
|
||||
break;
|
||||
|
||||
// Must not be called with a string that consists only of zero or more '\xff'
|
||||
// bytes.
|
||||
if (index < 0) {
|
||||
assert(false);
|
||||
}
|
||||
|
||||
auto r = std::span<uint8_t>(new (arena) uint8_t[index + 1], index + 1);
|
||||
memcpy(r.data(), key.data(), index + 1);
|
||||
r[r.size() - 1]++;
|
||||
return {{key.data(), int(key.size())}, {r.data(), int(r.size())}, 0};
|
||||
}
|
||||
|
||||
void benchConflictSet() {
|
||||
ankerl::nanobench::Bench bench;
|
||||
ConflictSet_ cs{0};
|
||||
ConflictSet cs{0};
|
||||
|
||||
bench.batch(kOpsPerTx);
|
||||
bench.minEpochIterations(2000);
|
||||
|
||||
int64_t version = 0;
|
||||
|
||||
@@ -678,10 +76,9 @@ template <class ConflictSet_> void benchConflictSet(const std::string &name) {
|
||||
w.begin.len = r.begin.len;
|
||||
w.end.p = r.end.p;
|
||||
w.end.len = 0;
|
||||
w.writeVersion = version + 1;
|
||||
writes.push_back(w);
|
||||
}
|
||||
cs.addWrites(writes.data(), writes.size());
|
||||
cs.addWrites(writes.data(), writes.size(), version + 1);
|
||||
++version;
|
||||
}
|
||||
|
||||
@@ -712,11 +109,10 @@ template <class ConflictSet_> void benchConflictSet(const std::string &name) {
|
||||
w.begin.len = begin.size();
|
||||
w.end.p = end.data();
|
||||
w.end.len = end.size();
|
||||
w.writeVersion = version + 1;
|
||||
writes.push_back(w);
|
||||
}
|
||||
cs.addWrites(writes.data(), kOpsPerTx, version + 1);
|
||||
++version;
|
||||
cs.addWrites(writes.data(), kOpsPerTx);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -732,7 +128,7 @@ template <class ConflictSet_> void benchConflictSet(const std::string &name) {
|
||||
|
||||
auto *results = new (arena) ConflictSet::Result[kOpsPerTx];
|
||||
|
||||
bench.run(name + " (point reads)",
|
||||
bench.run("point reads",
|
||||
[&]() { cs.check(reads.data(), results, kOpsPerTx); });
|
||||
}
|
||||
|
||||
@@ -748,7 +144,7 @@ template <class ConflictSet_> void benchConflictSet(const std::string &name) {
|
||||
|
||||
auto *results = new (arena) ConflictSet::Result[kOpsPerTx];
|
||||
|
||||
bench.run(name + " (prefix reads)",
|
||||
bench.run("prefix reads",
|
||||
[&]() { cs.check(reads.data(), results, kOpsPerTx); });
|
||||
}
|
||||
|
||||
@@ -769,7 +165,7 @@ template <class ConflictSet_> void benchConflictSet(const std::string &name) {
|
||||
|
||||
auto *results = new (arena) ConflictSet::Result[kOpsPerTx];
|
||||
|
||||
bench.run(name + " (range reads)",
|
||||
bench.run("range reads",
|
||||
[&]() { cs.check(reads.data(), results, kOpsPerTx); });
|
||||
}
|
||||
|
||||
@@ -787,13 +183,14 @@ template <class ConflictSet_> void benchConflictSet(const std::string &name) {
|
||||
++iter;
|
||||
}
|
||||
|
||||
bench.run(name + " (point writes)", [&]() {
|
||||
while (version < kMvccWindow) {
|
||||
auto v = ++version;
|
||||
for (auto &w : writes) {
|
||||
w.writeVersion = v;
|
||||
}
|
||||
cs.addWrites(writes.data(), writes.size());
|
||||
cs.setOldestVersion(std::max<int64_t>(version - kMvccWindow, 0));
|
||||
cs.addWrites(writes.data(), 1, v);
|
||||
}
|
||||
|
||||
bench.run("point writes", [&]() {
|
||||
auto v = ++version;
|
||||
cs.addWrites(writes.data(), writes.size(), v);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -811,13 +208,9 @@ template <class ConflictSet_> void benchConflictSet(const std::string &name) {
|
||||
++iter;
|
||||
}
|
||||
|
||||
bench.run(name + " (prefix writes)", [&]() {
|
||||
bench.run("prefix writes", [&]() {
|
||||
auto v = ++version;
|
||||
for (auto &w : writes) {
|
||||
w.writeVersion = v;
|
||||
}
|
||||
cs.addWrites(writes.data(), writes.size());
|
||||
cs.setOldestVersion(std::max<int64_t>(version - kMvccWindow, 0));
|
||||
cs.addWrites(writes.data(), writes.size(), v);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -835,45 +228,34 @@ template <class ConflictSet_> void benchConflictSet(const std::string &name) {
|
||||
writes.push_back(w);
|
||||
}
|
||||
|
||||
bench.run(name + " (range writes)", [&]() {
|
||||
bench.run("range writes", [&]() {
|
||||
auto v = ++version;
|
||||
for (auto &w : writes) {
|
||||
w.writeVersion = v;
|
||||
}
|
||||
cs.addWrites(writes.data(), writes.size());
|
||||
cs.setOldestVersion(std::max<int64_t>(version - kMvccWindow, 0));
|
||||
cs.addWrites(writes.data(), writes.size(), v);
|
||||
});
|
||||
}
|
||||
|
||||
bench.batch(1);
|
||||
|
||||
bench.warmup(10000);
|
||||
|
||||
{
|
||||
bench.run("monotonic increasing point writes", [&]() {
|
||||
auto v = ++version;
|
||||
ConflictSet::WriteRange w;
|
||||
|
||||
uint8_t b[9];
|
||||
b[8] = 0;
|
||||
auto x = __builtin_bswap64(version);
|
||||
memcpy(b, &x, 8);
|
||||
|
||||
w.begin.p = b;
|
||||
w.begin.len = 8;
|
||||
w.end.len = 0;
|
||||
w.end.p = b;
|
||||
cs.addWrites(&w, 1, v);
|
||||
cs.setOldestVersion(version - kMvccWindow);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
benchConflictSet<SkipListConflictSet>("skip list");
|
||||
benchConflictSet<ConflictSet>("radix tree");
|
||||
}
|
||||
|
||||
// extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
|
||||
// TestDriver<SkipListConflictSet> driver{data, size};
|
||||
|
||||
// for (;;) {
|
||||
// bool done = driver.next();
|
||||
// if (!driver.ok) {
|
||||
// // debugPrintDot(stdout, driver.cs.root);
|
||||
// // fflush(stdout);
|
||||
// abort();
|
||||
// }
|
||||
// #if DEBUG_VERBOSE && !defined(NDEBUG)
|
||||
// fprintf(stderr, "Check correctness\n");
|
||||
// #endif
|
||||
// // bool success = checkCorrectness(driver.cs.root);
|
||||
// // if (!success) {
|
||||
// // debugPrintDot(stdout, driver.cs.root);
|
||||
// // fflush(stdout);
|
||||
// // abort();
|
||||
// // }
|
||||
// if (done) {
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
|
||||
// return 0;
|
||||
// }
|
||||
int main(void) { benchConflictSet(); }
|
||||
|
153
CMakeLists.txt
153
CMakeLists.txt
@@ -1,9 +1,10 @@
|
||||
cmake_minimum_required(VERSION 3.18)
|
||||
project(
|
||||
conflict_set
|
||||
conflict-set
|
||||
VERSION 0.0.1
|
||||
DESCRIPTION
|
||||
"A data structure for optimistic concurrency control on ranges of bitwise-lexicographically-ordered keys."
|
||||
HOMEPAGE_URL "https://git.weaselab.dev/weaselab/conflict-set"
|
||||
LANGUAGES C CXX)
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
|
||||
@@ -21,7 +22,11 @@ if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||
"MinSizeRel" "RelWithDebInfo")
|
||||
endif()
|
||||
|
||||
add_compile_options(-fdata-sections -ffunction-sections)
|
||||
add_compile_options(-fdata-sections -ffunction-sections -Wswitch-enum
|
||||
-Werror=switch-enum)
|
||||
|
||||
option(USE_SIMD_FALLBACK
|
||||
"Use fallback implementations of functions that use SIMD" OFF)
|
||||
|
||||
# This is encouraged according to
|
||||
# https://valgrind.org/docs/manual/manual-core-adv.html#manual-core-adv.clientreq
|
||||
@@ -41,18 +46,20 @@ endif()
|
||||
include(CheckIncludeFileCXX)
|
||||
include(CMakePushCheckState)
|
||||
|
||||
cmake_push_check_state()
|
||||
list(APPEND CMAKE_REQUIRED_FLAGS -mavx)
|
||||
check_include_file_cxx("immintrin.h" HAS_AVX)
|
||||
if(HAS_AVX)
|
||||
add_compile_options(-mavx)
|
||||
add_compile_definitions(HAS_AVX)
|
||||
endif()
|
||||
cmake_pop_check_state()
|
||||
if(NOT USE_SIMD_FALLBACK)
|
||||
cmake_push_check_state()
|
||||
list(APPEND CMAKE_REQUIRED_FLAGS -mavx)
|
||||
check_include_file_cxx("immintrin.h" HAS_AVX)
|
||||
if(HAS_AVX)
|
||||
add_compile_options(-mavx)
|
||||
add_compile_definitions(HAS_AVX)
|
||||
endif()
|
||||
cmake_pop_check_state()
|
||||
|
||||
check_include_file_cxx("arm_neon.h" HAS_ARM_NEON)
|
||||
if(HAS_ARM_NEON)
|
||||
add_compile_definitions(HAS_ARM_NEON)
|
||||
check_include_file_cxx("arm_neon.h" HAS_ARM_NEON)
|
||||
if(HAS_ARM_NEON)
|
||||
add_compile_definitions(HAS_ARM_NEON)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
set(CMAKE_CXX_IMPLICIT_LINK_LIBRARIES "")
|
||||
@@ -64,6 +71,9 @@ target_include_directories(${PROJECT_NAME}_object
|
||||
PRIVATE ${CMAKE_SOURCE_DIR}/include)
|
||||
|
||||
add_library(${PROJECT_NAME} SHARED $<TARGET_OBJECTS:${PROJECT_NAME}_object>)
|
||||
set_target_properties(
|
||||
${PROJECT_NAME} PROPERTIES LIBRARY_OUTPUT_DIRECTORY
|
||||
"${CMAKE_BINARY_DIR}/radix_tree")
|
||||
if(NOT CMAKE_BUILD_TYPE STREQUAL Debug)
|
||||
set_target_properties(${PROJECT_NAME} PROPERTIES LINKER_LANGUAGE C)
|
||||
endif()
|
||||
@@ -73,27 +83,51 @@ if(NOT APPLE)
|
||||
LINKER:--version-script=${CMAKE_SOURCE_DIR}/linker.map)
|
||||
endif()
|
||||
|
||||
add_library(${PROJECT_NAME}_static STATIC
|
||||
add_library(${PROJECT_NAME}-static STATIC
|
||||
$<TARGET_OBJECTS:${PROJECT_NAME}_object>)
|
||||
if(NOT CMAKE_BUILD_TYPE STREQUAL Debug)
|
||||
set_target_properties(${PROJECT_NAME}_static PROPERTIES LINKER_LANGUAGE C)
|
||||
set_target_properties(${PROJECT_NAME}-static PROPERTIES LINKER_LANGUAGE C)
|
||||
endif()
|
||||
|
||||
if(NOT APPLE AND CMAKE_OBJCOPY)
|
||||
add_custom_command(
|
||||
TARGET conflict_set_static
|
||||
TARGET conflict-set-static
|
||||
POST_BUILD
|
||||
COMMAND
|
||||
${CMAKE_OBJCOPY} --keep-global-symbols=${CMAKE_SOURCE_DIR}/symbols.txt
|
||||
$<TARGET_FILE:${PROJECT_NAME}_static>)
|
||||
$<TARGET_FILE:${PROJECT_NAME}-static>)
|
||||
endif()
|
||||
|
||||
set(TEST_FLAGS -Wall -Wextra -Wpedantic -Wunreachable-code -UNDEBUG)
|
||||
set(TEST_FLAGS -Wall -Wextra -Wunreachable-code -Wpedantic -UNDEBUG)
|
||||
|
||||
include(CTest)
|
||||
|
||||
if(BUILD_TESTING)
|
||||
|
||||
# Shared library version of FoundationDB's skip list implementation
|
||||
add_library(skip_list SHARED SkipList.cpp)
|
||||
target_compile_options(skip_list PRIVATE -fPIC -fno-exceptions
|
||||
-fvisibility=hidden)
|
||||
target_include_directories(skip_list PUBLIC ${CMAKE_SOURCE_DIR}/include)
|
||||
set_target_properties(skip_list PROPERTIES LIBRARY_OUTPUT_DIRECTORY
|
||||
"${CMAKE_BINARY_DIR}/skip_list")
|
||||
set_target_properties(skip_list PROPERTIES OUTPUT_NAME ${PROJECT_NAME})
|
||||
set_target_properties(skip_list PROPERTIES VERSION ${PROJECT_VERSION}
|
||||
SOVERSION ${PROJECT_VERSION_MAJOR})
|
||||
|
||||
# Shared library version of a std::unordered_map-based conflict set (point
|
||||
# queries only)
|
||||
add_library(hash_table SHARED HashTable.cpp)
|
||||
target_compile_options(hash_table PRIVATE -fPIC -fno-exceptions
|
||||
-fvisibility=hidden)
|
||||
target_include_directories(hash_table PUBLIC ${CMAKE_SOURCE_DIR}/include)
|
||||
set_target_properties(hash_table PROPERTIES LIBRARY_OUTPUT_DIRECTORY
|
||||
"${CMAKE_BINARY_DIR}/hash_table")
|
||||
set_target_properties(hash_table PROPERTIES OUTPUT_NAME ${PROJECT_NAME})
|
||||
set_target_properties(
|
||||
hash_table PROPERTIES VERSION ${PROJECT_VERSION} SOVERSION
|
||||
${PROJECT_VERSION_MAJOR})
|
||||
|
||||
add_executable(conflict_set_main ConflictSet.cpp)
|
||||
target_include_directories(conflict_set_main
|
||||
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
|
||||
@@ -126,6 +160,10 @@ if(BUILD_TESTING)
|
||||
|
||||
add_executable(fuzz_driver ConflictSet.cpp FuzzTestDriver.cpp)
|
||||
target_compile_options(fuzz_driver PRIVATE ${TEST_FLAGS})
|
||||
if(NOT CMAKE_CROSSCOMPILING)
|
||||
target_compile_options(fuzz_driver PRIVATE -fsanitize=address,undefined)
|
||||
target_link_options(fuzz_driver PRIVATE -fsanitize=address,undefined)
|
||||
endif()
|
||||
target_compile_definitions(fuzz_driver PRIVATE ENABLE_FUZZ)
|
||||
target_include_directories(fuzz_driver
|
||||
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
|
||||
@@ -134,12 +172,41 @@ if(BUILD_TESTING)
|
||||
add_test(NAME conflict_set_fuzz_${hash} COMMAND fuzz_driver ${TEST})
|
||||
endforeach()
|
||||
|
||||
# tsan
|
||||
|
||||
if(NOT CMAKE_CROSSCOMPILING AND NOT CMAKE_BUILD_TYPE STREQUAL Debug)
|
||||
add_executable(tsan_driver ConflictSet.cpp FuzzTestDriver.cpp)
|
||||
target_compile_options(tsan_driver PRIVATE ${TEST_FLAGS} -fsanitize=thread)
|
||||
target_link_options(tsan_driver PRIVATE -fsanitize=thread)
|
||||
target_compile_definitions(tsan_driver PRIVATE ENABLE_FUZZ THREAD_TEST)
|
||||
target_include_directories(tsan_driver
|
||||
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
|
||||
foreach(TEST ${CORPUS_TESTS})
|
||||
get_filename_component(hash ${TEST} NAME)
|
||||
add_test(NAME conflict_set_tsan_${hash} COMMAND tsan_driver ${TEST})
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
add_executable(driver TestDriver.cpp)
|
||||
target_compile_options(driver PRIVATE ${TEST_FLAGS})
|
||||
target_link_libraries(driver PRIVATE ${PROJECT_NAME})
|
||||
|
||||
add_executable(script_test ScriptTest.cpp)
|
||||
target_compile_options(script_test PRIVATE ${TEST_FLAGS})
|
||||
target_link_libraries(script_test PRIVATE ${PROJECT_NAME})
|
||||
|
||||
file(GLOB SCRIPT_TESTS ${CMAKE_SOURCE_DIR}/script_tests/*)
|
||||
foreach(TEST ${SCRIPT_TESTS})
|
||||
get_filename_component(name ${TEST} NAME)
|
||||
add_test(NAME conflict_set_script_${name} COMMAND script_test ${TEST})
|
||||
endforeach()
|
||||
|
||||
add_executable(driver_skip_list TestDriver.cpp)
|
||||
target_compile_options(driver_skip_list PRIVATE ${TEST_FLAGS})
|
||||
target_link_libraries(driver_skip_list PRIVATE skip_list)
|
||||
|
||||
find_program(VALGRIND_EXE valgrind)
|
||||
if(VALGRIND_EXE)
|
||||
if(VALGRIND_EXE AND NOT CMAKE_CROSSCOMPILING)
|
||||
add_test(NAME conflict_set_blackbox_valgrind
|
||||
COMMAND ${VALGRIND_EXE} --error-exitcode=99 --
|
||||
$<TARGET_FILE:driver> ${CORPUS_TESTS})
|
||||
@@ -148,6 +215,7 @@ if(BUILD_TESTING)
|
||||
foreach(TEST ${CORPUS_TESTS})
|
||||
get_filename_component(hash ${TEST} NAME)
|
||||
add_test(NAME conflict_set_blackbox_${hash} COMMAND driver ${TEST})
|
||||
add_test(NAME skip_list_${hash} COMMAND driver_skip_list ${TEST})
|
||||
endforeach()
|
||||
|
||||
# api smoke tests
|
||||
@@ -156,17 +224,18 @@ if(BUILD_TESTING)
|
||||
add_executable(conflict_set_c_api_test conflict_set_c_api_test.c)
|
||||
target_compile_options(conflict_set_c_api_test PRIVATE ${TEST_FLAGS})
|
||||
target_link_libraries(conflict_set_c_api_test PRIVATE ${PROJECT_NAME})
|
||||
set_property(TARGET conflict_set_c_api_test PROPERTY C_STANDARD 90)
|
||||
set_property(TARGET conflict_set_c_api_test PROPERTY C_STANDARD_REQUIRED ON)
|
||||
set_target_properties(conflict_set_c_api_test PROPERTIES C_STANDARD 90)
|
||||
set_target_properties(conflict_set_c_api_test PROPERTIES C_STANDARD_REQUIRED
|
||||
ON)
|
||||
add_test(NAME conflict_set_c_api_test COMMAND conflict_set_c_api_test)
|
||||
|
||||
# c++98
|
||||
add_executable(conflict_set_cxx_api_test conflict_set_cxx_api_test.cpp)
|
||||
target_compile_options(conflict_set_cxx_api_test PRIVATE ${TEST_FLAGS})
|
||||
target_link_libraries(conflict_set_cxx_api_test PRIVATE ${PROJECT_NAME})
|
||||
set_property(TARGET conflict_set_cxx_api_test PROPERTY CXX_STANDARD 98)
|
||||
set_property(TARGET conflict_set_cxx_api_test PROPERTY CXX_STANDARD_REQUIRED
|
||||
ON)
|
||||
set_target_properties(conflict_set_cxx_api_test PROPERTIES CXX_STANDARD 98)
|
||||
set_target_properties(conflict_set_cxx_api_test
|
||||
PROPERTIES CXX_STANDARD_REQUIRED ON)
|
||||
add_test(NAME conflict_set_cxx_api_test COMMAND conflict_set_cxx_api_test)
|
||||
|
||||
if(NOT APPLE AND NOT CMAKE_BUILD_TYPE STREQUAL Debug)
|
||||
@@ -178,22 +247,36 @@ if(BUILD_TESTING)
|
||||
NAME conflict_set_static_symbols
|
||||
COMMAND
|
||||
${CMAKE_SOURCE_DIR}/test_symbols.sh
|
||||
$<TARGET_FILE:${PROJECT_NAME}_static> ${CMAKE_SOURCE_DIR}/symbols.txt)
|
||||
$<TARGET_FILE:${PROJECT_NAME}-static> ${CMAKE_SOURCE_DIR}/symbols.txt)
|
||||
endif()
|
||||
|
||||
# bench
|
||||
|
||||
add_executable(conflict_set_bench Bench.cpp)
|
||||
target_link_libraries(conflict_set_bench PRIVATE ${PROJECT_NAME})
|
||||
# target_compile_options(conflict_set_bench PRIVATE
|
||||
# "-fsanitize=address,undefined,fuzzer")
|
||||
# target_link_options(conflict_set_bench PRIVATE
|
||||
# "-fsanitize=address,undefined,fuzzer")
|
||||
set_target_properties(conflict_set_bench PROPERTIES SKIP_BUILD_RPATH ON)
|
||||
|
||||
add_executable(real_data_bench RealDataBench.cpp)
|
||||
target_link_libraries(real_data_bench PRIVATE ${PROJECT_NAME})
|
||||
set_target_properties(real_data_bench PROPERTIES SKIP_BUILD_RPATH ON)
|
||||
endif()
|
||||
# packaging
|
||||
|
||||
set(CPACK_PACKAGE_CONTACT andrew@weaselab.dev)
|
||||
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
|
||||
|
||||
set(CPACK_PACKAGE_VENDOR "Weaselab")
|
||||
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE")
|
||||
set(CPACK_RESOURCE_FILE_README "${CMAKE_CURRENT_SOURCE_DIR}/README.md")
|
||||
|
||||
# rpm
|
||||
set(CPACK_RPM_PACKAGE_ARCHITECTURE ${CMAKE_SYSTEM_PROCESSOR})
|
||||
set(CPACK_RPM_SPEC_INSTALL_POST "/bin/true") # avoid stripping
|
||||
set(CPACK_RPM_PACKAGE_LICENSE "Apache 2.0")
|
||||
set(CPACK_RPM_FILE_NAME RPM-DEFAULT)
|
||||
|
||||
# deb
|
||||
set(CPACK_DEBIAN_FILE_NAME DEB-DEFAULT)
|
||||
|
||||
include(CPack)
|
||||
|
||||
include(GNUInstallDirs)
|
||||
@@ -204,7 +287,7 @@ target_include_directories(
|
||||
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}>)
|
||||
|
||||
target_include_directories(
|
||||
${PROJECT_NAME}_static
|
||||
${PROJECT_NAME}-static
|
||||
PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
|
||||
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}>)
|
||||
|
||||
@@ -213,13 +296,13 @@ set_target_properties(
|
||||
SOVERSION ${PROJECT_VERSION_MAJOR})
|
||||
|
||||
install(
|
||||
TARGETS ${PROJECT_NAME} ${PROJECT_NAME}_static
|
||||
EXPORT ConflictSetConfig
|
||||
TARGETS ${PROJECT_NAME} ${PROJECT_NAME}-static
|
||||
EXPORT conflict-setConfig
|
||||
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
||||
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
|
||||
install(DIRECTORY include/
|
||||
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME})
|
||||
|
||||
install(EXPORT ConflictSetConfig
|
||||
DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/ConflictSet/cmake)
|
||||
install(EXPORT conflict-setConfig
|
||||
DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/conflict-set/cmake)
|
||||
|
2649
ConflictSet.cpp
2649
ConflictSet.cpp
File diff suppressed because it is too large
Load Diff
53
Dockerfile
53
Dockerfile
@@ -15,6 +15,7 @@ RUN TZ=America/Los_Angeles DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
||||
curl \
|
||||
doxygen \
|
||||
file \
|
||||
g++-aarch64-linux-gnu \
|
||||
gcovr \
|
||||
git \
|
||||
gperf \
|
||||
@@ -23,17 +24,20 @@ RUN TZ=America/Los_Angeles DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
||||
ninja-build \
|
||||
pre-commit \
|
||||
python3-requests \
|
||||
qemu-user \
|
||||
rpm \
|
||||
texlive-full \
|
||||
zstd
|
||||
|
||||
# Install recent valgrind from source
|
||||
RUN curl -Ls https://sourceware.org/pub/valgrind/valgrind-3.20.0.tar.bz2 -o valgrind.tar.bz2 && \
|
||||
echo "8536c031dbe078d342f121fa881a9ecd205cb5a78e639005ad570011bdb9f3c6 valgrind.tar.bz2" > valgrind-sha.txt && \
|
||||
RUN curl -Ls https://sourceware.org/pub/valgrind/valgrind-3.22.0.tar.bz2 -o valgrind.tar.bz2 && \
|
||||
echo "c811db5add2c5f729944caf47c4e7a65dcaabb9461e472b578765dd7bf6d2d4c valgrind.tar.bz2" > valgrind-sha.txt && \
|
||||
sha256sum --quiet -c valgrind-sha.txt && \
|
||||
mkdir valgrind && \
|
||||
tar --strip-components 1 --no-same-owner --no-same-permissions --directory valgrind -xjf valgrind.tar.bz2 && \
|
||||
cd valgrind && \
|
||||
./configure --enable-only64bit --enable-lto && \
|
||||
make && \
|
||||
make -j`nproc` && \
|
||||
make install && \
|
||||
cd .. && \
|
||||
rm -rf /tmp/*
|
||||
@@ -42,49 +46,6 @@ RUN curl -Ls https://sourceware.org/pub/valgrind/valgrind-3.20.0.tar.bz2 -o valg
|
||||
ENV CC=clang
|
||||
ENV CXX=clang++
|
||||
|
||||
# Install recent flatbuffers from source
|
||||
RUN curl -Ls https://github.com/google/flatbuffers/archive/refs/tags/v23.3.3.tar.gz -o flatbuffers.tar.gz && \
|
||||
echo "8aff985da30aaab37edf8e5b02fda33ed4cbdd962699a8e2af98fdef306f4e4d flatbuffers.tar.gz" > flatbuffers-sha.txt && \
|
||||
sha256sum --quiet -c flatbuffers-sha.txt && \
|
||||
mkdir flatbuffers && \
|
||||
tar --strip-components 1 --no-same-owner --no-same-permissions --directory flatbuffers -xf flatbuffers.tar.gz && \
|
||||
cd flatbuffers && \
|
||||
cmake -S. -B build -G Ninja -DCMAKE_BUILD_TYPE=Release && \
|
||||
ninja -C build install && \
|
||||
rm -rf /tmp/*
|
||||
|
||||
# Build msan-instrumented libc++ (llvmorg-16.0.0)
|
||||
RUN curl -Ls https://github.com/llvm/llvm-project/releases/download/llvmorg-16.0.0/llvm-project-16.0.0.src.tar.xz -o llvm-project.tar.gz && \
|
||||
echo "9a56d906a2c81f16f06efc493a646d497c53c2f4f28f0cb1f3c8da7f74350254 llvm-project.tar.gz" > llvm-project-sha.txt && \
|
||||
sha256sum --quiet -c llvm-project-sha.txt && \
|
||||
mkdir llvm-project && \
|
||||
tar --strip-components 1 --no-same-owner --no-same-permissions --directory llvm-project -xf llvm-project.tar.gz && \
|
||||
cmake -Sllvm-project/runtimes -B build -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DLLVM_ENABLE_RUNTIMES="libcxx;libcxxabi" \
|
||||
-DCMAKE_C_COMPILER=clang \
|
||||
-DCMAKE_CXX_COMPILER=clang++ \
|
||||
-DCMAKE_INSTALL_PREFIX=/opt/llvm-msan \
|
||||
-DLLVM_USE_SANITIZER=MemoryWithOrigins && \
|
||||
ninja -C build cxx cxxabi && \
|
||||
ninja -C build install-cxx install-cxxabi && \
|
||||
rm -rf /tmp/*
|
||||
|
||||
# Install bloaty from source
|
||||
RUN curl -Ls https://github.com/google/bloaty/releases/download/v1.1/bloaty-1.1.tar.bz2 -o bloaty.tar.bz2 && \
|
||||
echo "a308d8369d5812aba45982e55e7c3db2ea4780b7496a5455792fb3dcba9abd6f bloaty.tar.bz2" > bloaty-sha.txt && \
|
||||
sha256sum --quiet -c bloaty-sha.txt && \
|
||||
mkdir bloaty && \
|
||||
tar --strip-components 1 --no-same-owner --no-same-permissions --directory bloaty -xf bloaty.tar.bz2 && \
|
||||
cd bloaty && \
|
||||
cmake -S. -B build -G Ninja && \
|
||||
ninja -C build install && \
|
||||
rm -rf /tmp/*
|
||||
|
||||
RUN apt-get update
|
||||
RUN apt-get upgrade -y
|
||||
RUN TZ=America/Los_Angeles DEBIAN_FRONTEND=noninteractive apt-get install -y texlive-full
|
||||
|
||||
# Try to have all the pre-commit hooks we'll need already initialized
|
||||
COPY .pre-commit-config.yaml /tmp/
|
||||
RUN git init && pre-commit install-hooks
|
||||
|
@@ -2,15 +2,25 @@
|
||||
#include <cstdint>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <thread>
|
||||
|
||||
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size);
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
std::ifstream t(argv[i], std::ios::binary);
|
||||
std::stringstream buffer;
|
||||
buffer << t.rdbuf();
|
||||
auto str = buffer.str();
|
||||
LLVMFuzzerTestOneInput((const uint8_t *)str.data(), str.size());
|
||||
}
|
||||
auto doTest = [&]() {
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
std::ifstream t(argv[i], std::ios::binary);
|
||||
std::stringstream buffer;
|
||||
buffer << t.rdbuf();
|
||||
auto str = buffer.str();
|
||||
LLVMFuzzerTestOneInput((const uint8_t *)str.data(), str.size());
|
||||
}
|
||||
};
|
||||
#ifdef THREAD_TEST
|
||||
std::thread thread2{doTest};
|
||||
#endif
|
||||
doTest();
|
||||
#ifdef THREAD_TEST
|
||||
thread2.join();
|
||||
#endif
|
||||
}
|
||||
|
154
HashTable.cpp
Normal file
154
HashTable.cpp
Normal file
@@ -0,0 +1,154 @@
|
||||
#include "ConflictSet.h"
|
||||
#include "Internal.h"
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
// This implementation isn't correct for range queries :). It's just intended as
|
||||
// a reference for performance comparison with point queries.
|
||||
|
||||
// struct is from "https://www.cppstories.com/2021/heterogeneous-access-cpp20/"
|
||||
struct string_hash {
|
||||
using is_transparent = void;
|
||||
[[nodiscard]] size_t operator()(std::string_view txt) const {
|
||||
return std::hash<std::string_view>{}(txt);
|
||||
}
|
||||
[[nodiscard]] size_t operator()(const std::string &txt) const {
|
||||
return std::hash<std::string>{}(txt);
|
||||
}
|
||||
};
|
||||
|
||||
struct __attribute__((visibility("hidden"))) ConflictSet::Impl {
|
||||
Impl(int64_t oldestVersion) : oldestVersion(oldestVersion) {}
|
||||
void check(const ConflictSet::ReadRange *reads, ConflictSet::Result *results,
|
||||
int count) const {
|
||||
for (int i = 0; i < count; ++i) {
|
||||
auto key =
|
||||
std::string_view((const char *)reads[i].begin.p, reads[i].begin.len);
|
||||
auto version = reads[i].readVersion;
|
||||
if (version < oldestVersion) {
|
||||
results[i] = TooOld;
|
||||
continue;
|
||||
}
|
||||
auto iter = map.find(key);
|
||||
results[i] =
|
||||
iter == map.end() || iter->second <= version ? Commit : Conflict;
|
||||
}
|
||||
}
|
||||
|
||||
void addWrites(const ConflictSet::WriteRange *writes, int count,
|
||||
int64_t writeVersion) {
|
||||
for (int i = 0; i < count; ++i) {
|
||||
auto &max = map[std::string((const char *)writes[i].begin.p,
|
||||
writes[i].begin.len)];
|
||||
assert(writeVersion >= max);
|
||||
max = writeVersion;
|
||||
keyUpdates += 2;
|
||||
}
|
||||
}
|
||||
|
||||
void setOldestVersion(int64_t oldestVersion) {
|
||||
if (oldestVersion <= this->oldestVersion) {
|
||||
return;
|
||||
}
|
||||
this->oldestVersion = oldestVersion;
|
||||
if (keyUpdates < 100) {
|
||||
return;
|
||||
}
|
||||
auto iter = map.find(removalKey);
|
||||
while (keyUpdates > 0) {
|
||||
if (iter == map.end()) {
|
||||
iter = map.begin();
|
||||
}
|
||||
for (; iter != map.end(); --keyUpdates) {
|
||||
if (iter->second <= oldestVersion) {
|
||||
iter = map.erase(iter);
|
||||
} else {
|
||||
++iter;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (iter == map.end()) {
|
||||
removalKey.clear();
|
||||
} else {
|
||||
removalKey = iter->first;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
int64_t keyUpdates = 0;
|
||||
int64_t oldestVersion;
|
||||
std::unordered_map<std::string, int64_t, string_hash, std::equal_to<>> map;
|
||||
std::string removalKey;
|
||||
};
|
||||
|
||||
void ConflictSet::check(const ReadRange *reads, Result *results,
|
||||
int count) const {
|
||||
return impl->check(reads, results, count);
|
||||
}
|
||||
|
||||
void ConflictSet::addWrites(const WriteRange *writes, int count,
|
||||
int64_t writeVersion) {
|
||||
return impl->addWrites(writes, count, writeVersion);
|
||||
}
|
||||
|
||||
void ConflictSet::setOldestVersion(int64_t oldestVersion) {
|
||||
return impl->setOldestVersion(oldestVersion);
|
||||
}
|
||||
|
||||
int64_t ConflictSet::getBytes() const { return -1; }
|
||||
|
||||
ConflictSet::ConflictSet(int64_t oldestVersion)
|
||||
: impl(new (safe_malloc(sizeof(Impl))) Impl{oldestVersion}) {}
|
||||
|
||||
ConflictSet::~ConflictSet() {
|
||||
if (impl) {
|
||||
impl->~Impl();
|
||||
safe_free(impl, sizeof(Impl));
|
||||
}
|
||||
}
|
||||
|
||||
ConflictSet::ConflictSet(ConflictSet &&other) noexcept
|
||||
: impl(std::exchange(other.impl, nullptr)) {}
|
||||
|
||||
ConflictSet &ConflictSet::operator=(ConflictSet &&other) noexcept {
|
||||
impl = std::exchange(other.impl, nullptr);
|
||||
return *this;
|
||||
}
|
||||
|
||||
using ConflictSet_Result = ConflictSet::Result;
|
||||
using ConflictSet_Key = ConflictSet::Key;
|
||||
using ConflictSet_ReadRange = ConflictSet::ReadRange;
|
||||
using ConflictSet_WriteRange = ConflictSet::WriteRange;
|
||||
|
||||
extern "C" {
|
||||
__attribute__((__visibility__("default"))) void
|
||||
ConflictSet_check(void *cs, const ConflictSet_ReadRange *reads,
|
||||
ConflictSet_Result *results, int count) {
|
||||
((ConflictSet::Impl *)cs)->check(reads, results, count);
|
||||
}
|
||||
__attribute__((__visibility__("default"))) void
|
||||
ConflictSet_addWrites(void *cs, const ConflictSet_WriteRange *writes, int count,
|
||||
int64_t writeVersion) {
|
||||
((ConflictSet::Impl *)cs)->addWrites(writes, count, writeVersion);
|
||||
}
|
||||
__attribute__((__visibility__("default"))) void
|
||||
ConflictSet_setOldestVersion(void *cs, int64_t oldestVersion) {
|
||||
((ConflictSet::Impl *)cs)->setOldestVersion(oldestVersion);
|
||||
}
|
||||
__attribute__((__visibility__("default"))) void *
|
||||
ConflictSet_create(int64_t oldestVersion) {
|
||||
return new (safe_malloc(sizeof(ConflictSet::Impl)))
|
||||
ConflictSet::Impl{oldestVersion};
|
||||
}
|
||||
__attribute__((__visibility__("default"))) void ConflictSet_destroy(void *cs) {
|
||||
using Impl = ConflictSet::Impl;
|
||||
((Impl *)cs)->~Impl();
|
||||
safe_free(cs, sizeof(Impl));
|
||||
}
|
||||
__attribute__((__visibility__("default"))) int64_t
|
||||
ConflictSet_getBytes(void *cs) {
|
||||
using Impl = ConflictSet::Impl;
|
||||
return -1;
|
||||
}
|
||||
}
|
214
Internal.h
214
Internal.h
@@ -2,6 +2,8 @@
|
||||
|
||||
#include "ConflictSet.h"
|
||||
|
||||
using namespace weaselab;
|
||||
|
||||
#include <bit>
|
||||
#include <cassert>
|
||||
#include <compare>
|
||||
@@ -10,10 +12,12 @@
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <inttypes.h>
|
||||
#include <latch>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
@@ -21,6 +25,7 @@
|
||||
#include <callgrind.h>
|
||||
|
||||
#define DEBUG_VERBOSE 0
|
||||
#define SHOW_MEMORY 0
|
||||
|
||||
[[nodiscard]] inline auto
|
||||
operator<=>(const std::span<const uint8_t> &lhs,
|
||||
@@ -34,16 +39,76 @@ operator<=>(const std::span<const uint8_t> &lhs,
|
||||
return lhs.size() <=> rhs.size();
|
||||
}
|
||||
|
||||
[[nodiscard]] inline auto operator<=>(const std::span<const uint8_t> &lhs,
|
||||
const ConflictSet::Key &rhs) noexcept {
|
||||
int cl = std::min<int>(lhs.size(), rhs.len);
|
||||
if (cl > 0) {
|
||||
if (auto c = memcmp(lhs.data(), rhs.p, cl) <=> 0; c != 0) {
|
||||
return c;
|
||||
}
|
||||
}
|
||||
return lhs.size() <=> size_t(rhs.len);
|
||||
}
|
||||
|
||||
// This header contains code that we want to reuse outside of ConflictSet.cpp or
|
||||
// want to exclude from coverage since it's only testing related.
|
||||
|
||||
// GCOVR_EXCL_START
|
||||
|
||||
#if SHOW_MEMORY
|
||||
inline int64_t mallocBytes = 0;
|
||||
inline int64_t peakMallocBytes = 0;
|
||||
#endif
|
||||
|
||||
inline thread_local int64_t mallocBytesDelta = 0;
|
||||
|
||||
#ifndef NDEBUG
|
||||
constexpr auto kMallocHeaderSize = 16;
|
||||
#endif
|
||||
|
||||
// malloc that aborts on OOM and thus always returns a non-null pointer. Must be
|
||||
// paired with `safe_free`.
|
||||
__attribute__((always_inline)) inline void *safe_malloc(size_t s) {
|
||||
if (void *p = malloc(s)) {
|
||||
return p;
|
||||
mallocBytesDelta += s;
|
||||
#if SHOW_MEMORY
|
||||
mallocBytes += s;
|
||||
if (mallocBytes > peakMallocBytes) {
|
||||
peakMallocBytes = mallocBytes;
|
||||
}
|
||||
abort();
|
||||
#endif
|
||||
void *p = malloc(s
|
||||
#ifndef NDEBUG
|
||||
+ kMallocHeaderSize
|
||||
#endif
|
||||
);
|
||||
if (p == nullptr) {
|
||||
abort();
|
||||
}
|
||||
#ifndef NDEBUG
|
||||
memcpy(p, &s, sizeof(s));
|
||||
(char *&)p += kMallocHeaderSize;
|
||||
#endif
|
||||
return p;
|
||||
}
|
||||
|
||||
// Must be paired with `safe_malloc`.
|
||||
//
|
||||
// There's nothing safer about this than free. Only called safe_free for
|
||||
// symmetry with safe_malloc.
|
||||
__attribute__((always_inline)) inline void safe_free(void *p, size_t s) {
|
||||
mallocBytesDelta -= s;
|
||||
#if SHOW_MEMORY
|
||||
mallocBytes -= s;
|
||||
free(p);
|
||||
#else
|
||||
#ifndef NDEBUG
|
||||
(char *&)p -= kMallocHeaderSize;
|
||||
size_t expected;
|
||||
memcpy(&expected, p, sizeof(expected));
|
||||
assert(s == expected);
|
||||
#endif
|
||||
free(p);
|
||||
#endif
|
||||
}
|
||||
|
||||
// ==================== BEGIN ARENA IMPL ====================
|
||||
@@ -128,7 +193,7 @@ inline Arena::Arena(int initialSize) : impl(nullptr) {
|
||||
inline void onDestroy(Arena::ArenaImpl *impl) {
|
||||
while (impl) {
|
||||
auto *prev = impl->prev;
|
||||
free(impl);
|
||||
safe_free(impl, sizeof(Arena::ArenaImpl) + impl->capacity);
|
||||
impl = prev;
|
||||
}
|
||||
}
|
||||
@@ -348,34 +413,6 @@ inline uint32_t Arbitrary::bounded(uint32_t s) {
|
||||
|
||||
// ==================== END ARBITRARY IMPL ====================
|
||||
|
||||
// ==================== BEGIN UTILITIES IMPL ====================
|
||||
|
||||
// Call Stepwise::step for each element of remaining until it returns true.
|
||||
// Applies a permutation to `remaining` as a side effect.
|
||||
template <class Stepwise> void runInterleaved(std::span<Stepwise> remaining) {
|
||||
while (remaining.size() > 0) {
|
||||
for (int i = 0; i < int(remaining.size());) {
|
||||
bool done = remaining[i].step();
|
||||
if (done) {
|
||||
if (i != int(remaining.size()) - 1) {
|
||||
using std::swap;
|
||||
swap(remaining[i], remaining.back());
|
||||
}
|
||||
remaining = remaining.subspan(0, remaining.size() - 1);
|
||||
} else {
|
||||
++i;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
template <class Stepwise> void runSequential(std::span<Stepwise> remaining) {
|
||||
for (auto &r : remaining) {
|
||||
while (!r.step()) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ReferenceImpl {
|
||||
explicit ReferenceImpl(int64_t oldestVersion) : oldestVersion(oldestVersion) {
|
||||
writeVersionMap[""] = oldestVersion;
|
||||
@@ -403,7 +440,8 @@ struct ReferenceImpl {
|
||||
: ConflictSet::Commit;
|
||||
}
|
||||
}
|
||||
void addWrites(const ConflictSet::WriteRange *writes, int count) {
|
||||
void addWrites(const ConflictSet::WriteRange *writes, int count,
|
||||
int64_t writeVersion) {
|
||||
for (int i = 0; i < count; ++i) {
|
||||
auto begin =
|
||||
std::string((const char *)writes[i].begin.p, writes[i].begin.len);
|
||||
@@ -411,7 +449,6 @@ struct ReferenceImpl {
|
||||
writes[i].end.len == 0
|
||||
? begin + std::string("\x00", 1)
|
||||
: std::string((const char *)writes[i].end.p, writes[i].end.len);
|
||||
auto writeVersion = writes[i].writeVersion;
|
||||
auto prevVersion = (--writeVersionMap.upper_bound(end))->second;
|
||||
for (auto iter = writeVersionMap.lower_bound(begin),
|
||||
endIter = writeVersionMap.lower_bound(end);
|
||||
@@ -434,6 +471,10 @@ struct ReferenceImpl {
|
||||
|
||||
using Key = ConflictSet::Key;
|
||||
|
||||
inline Key operator"" _s(const char *str, size_t size) {
|
||||
return {reinterpret_cast<const uint8_t *>(str), int(size)};
|
||||
}
|
||||
|
||||
[[maybe_unused]] static Key toKey(Arena &arena, int n) {
|
||||
uint8_t *buf = new (arena) uint8_t[sizeof(n)];
|
||||
memcpy(buf, &n, sizeof(n));
|
||||
@@ -465,6 +506,18 @@ inline std::string printable(std::span<const uint8_t> key) {
|
||||
return printable(std::string_view((const char *)key.data(), key.size()));
|
||||
}
|
||||
|
||||
inline const char *resultToStr(ConflictSet::Result r) {
|
||||
switch (r) {
|
||||
case ConflictSet::Commit:
|
||||
return "commit";
|
||||
case ConflictSet::Conflict:
|
||||
return "conflict";
|
||||
case ConflictSet::TooOld:
|
||||
return "too old";
|
||||
}
|
||||
abort();
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
template <class ConflictSetImpl> struct TestDriver {
|
||||
@@ -477,22 +530,10 @@ template <class ConflictSetImpl> struct TestDriver {
|
||||
ConflictSetImpl cs{oldestVersion};
|
||||
ReferenceImpl refImpl{oldestVersion};
|
||||
|
||||
constexpr static auto kMaxKeyLen = 32;
|
||||
constexpr static auto kMaxKeyLen = 8;
|
||||
|
||||
bool ok = true;
|
||||
|
||||
static const char *resultToStr(ConflictSet::Result r) {
|
||||
switch (r) {
|
||||
case ConflictSet::Commit:
|
||||
return "commit";
|
||||
case ConflictSet::Conflict:
|
||||
return "conflict";
|
||||
case ConflictSet::TooOld:
|
||||
return "too old";
|
||||
}
|
||||
abort();
|
||||
}
|
||||
|
||||
// Call until it returns true, for "done". Check internal invariants etc
|
||||
// between calls to next.
|
||||
bool next() {
|
||||
@@ -542,17 +583,14 @@ template <class ConflictSetImpl> struct TestDriver {
|
||||
++iter;
|
||||
--rangesRemaining;
|
||||
}
|
||||
writes[i].writeVersion = v;
|
||||
#if DEBUG_VERBOSE && !defined(NDEBUG)
|
||||
if (writes[i].end.len == 0) {
|
||||
fprintf(stderr, "Write: {%s} -> %d\n",
|
||||
printable(writes[i].begin).c_str(),
|
||||
int(writes[i].writeVersion));
|
||||
fprintf(stderr, "Write: {%s} -> %" PRId64 "\n",
|
||||
printable(writes[i].begin).c_str(), writeVersion);
|
||||
} else {
|
||||
fprintf(stderr, "Write: [%s, %s) -> %d\n",
|
||||
fprintf(stderr, "Write: [%s, %s) -> %" PRId64 "\n",
|
||||
printable(writes[i].begin).c_str(),
|
||||
printable(writes[i].end).c_str(),
|
||||
int(writes[i].writeVersion));
|
||||
printable(writes[i].end).c_str(), writeVersion);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -560,10 +598,10 @@ template <class ConflictSetImpl> struct TestDriver {
|
||||
assert(i == numPointWrites + numRangeWrites);
|
||||
|
||||
CALLGRIND_START_INSTRUMENTATION;
|
||||
cs.addWrites(writes, numPointWrites + numRangeWrites);
|
||||
cs.addWrites(writes, numPointWrites + numRangeWrites, v);
|
||||
CALLGRIND_STOP_INSTRUMENTATION;
|
||||
|
||||
refImpl.addWrites(writes, numPointWrites + numRangeWrites);
|
||||
refImpl.addWrites(writes, numPointWrites + numRangeWrites, v);
|
||||
|
||||
oldestVersion = std::max<int64_t>(writeVersion - arbitrary.bounded(10),
|
||||
oldestVersion);
|
||||
@@ -630,32 +668,60 @@ template <class ConflictSetImpl> struct TestDriver {
|
||||
auto *results2 =
|
||||
new (arena) ConflictSet::Result[numPointReads + numRangeReads];
|
||||
|
||||
#ifdef THREAD_TEST
|
||||
auto *results3 =
|
||||
new (arena) ConflictSet::Result[numPointReads + numRangeReads];
|
||||
std::latch ready{1};
|
||||
std::thread thread2{[&]() {
|
||||
ready.count_down();
|
||||
cs.check(reads, results3, numPointReads + numRangeReads);
|
||||
}};
|
||||
ready.wait();
|
||||
#endif
|
||||
|
||||
CALLGRIND_START_INSTRUMENTATION;
|
||||
cs.check(reads, results1, numPointReads + numRangeReads);
|
||||
CALLGRIND_STOP_INSTRUMENTATION;
|
||||
|
||||
refImpl.check(reads, results2, numPointReads + numRangeReads);
|
||||
for (int i = 0; i < numPointReads + numRangeReads; ++i) {
|
||||
if (results1[i] != results2[i]) {
|
||||
if (reads[i].end.len == 0) {
|
||||
fprintf(stderr,
|
||||
"Expected %s, got %s for read of {%s} at version %" PRId64
|
||||
"\n",
|
||||
resultToStr(results2[i]), resultToStr(results1[i]),
|
||||
printable(reads[i].begin).c_str(), reads[i].readVersion);
|
||||
} else {
|
||||
fprintf(
|
||||
stderr,
|
||||
"Expected %s, got %s for read of [%s, %s) at version %" PRId64
|
||||
"\n",
|
||||
resultToStr(results2[i]), resultToStr(results1[i]),
|
||||
printable(reads[i].begin).c_str(),
|
||||
printable(reads[i].end).c_str(), reads[i].readVersion);
|
||||
|
||||
auto compareResults = [reads](ConflictSet::Result *results1,
|
||||
ConflictSet::Result *results2, int count) {
|
||||
for (int i = 0; i < count; ++i) {
|
||||
if (results1[i] != results2[i]) {
|
||||
if (reads[i].end.len == 0) {
|
||||
fprintf(stderr,
|
||||
"Expected %s, got %s for read of {%s} at version %" PRId64
|
||||
"\n",
|
||||
resultToStr(results2[i]), resultToStr(results1[i]),
|
||||
printable(reads[i].begin).c_str(), reads[i].readVersion);
|
||||
} else {
|
||||
fprintf(
|
||||
stderr,
|
||||
"Expected %s, got %s for read of [%s, %s) at version %" PRId64
|
||||
"\n",
|
||||
resultToStr(results2[i]), resultToStr(results1[i]),
|
||||
printable(reads[i].begin).c_str(),
|
||||
printable(reads[i].end).c_str(), reads[i].readVersion);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
ok = false;
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
if (!compareResults(results1, results2, numPointReads + numRangeReads)) {
|
||||
ok = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifdef THREAD_TEST
|
||||
thread2.join();
|
||||
if (!compareResults(results3, results2, numPointReads + numRangeReads)) {
|
||||
ok = false;
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
50
Jenkinsfile
vendored
50
Jenkinsfile
vendored
@@ -36,6 +36,29 @@ pipeline {
|
||||
sh 'pre-commit run --all-files --show-diff-on-failure'
|
||||
}
|
||||
}
|
||||
stage('Clang') {
|
||||
agent {
|
||||
dockerfile {
|
||||
args '-v /home/jenkins/ccache:/ccache'
|
||||
reuseNode true
|
||||
}
|
||||
}
|
||||
steps {
|
||||
CleanBuildAndTest("")
|
||||
recordIssues(tools: [clang()])
|
||||
}
|
||||
}
|
||||
stage('SIMD fallback') {
|
||||
agent {
|
||||
dockerfile {
|
||||
args '-v /home/jenkins/ccache:/ccache'
|
||||
reuseNode true
|
||||
}
|
||||
}
|
||||
steps {
|
||||
CleanBuildAndTest("-DUSE_SIMD_FALLBACK=ON")
|
||||
}
|
||||
}
|
||||
stage('Release [gcc]') {
|
||||
agent {
|
||||
dockerfile {
|
||||
@@ -44,18 +67,35 @@ pipeline {
|
||||
}
|
||||
}
|
||||
steps {
|
||||
CleanBuildAndTest("-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++ -DCMAKE_BUILD_TYPE=Release")
|
||||
CleanBuildAndTest("-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++ -DCMAKE_CXX_FLAGS=-DNVALGRIND")
|
||||
recordIssues(tools: [gcc()])
|
||||
sh '''
|
||||
cd build
|
||||
cpack -G DEB
|
||||
cpack -G RPM
|
||||
'''
|
||||
minio bucket: 'jenkins', credentialsId: 'jenkins-minio', excludes: '', host: 'minio.weaselab.dev', includes: 'build/*.deb', targetFolder: '${JOB_NAME}/${BUILD_NUMBER}/${STAGE_NAME}/'
|
||||
sh '''
|
||||
cd paper
|
||||
make
|
||||
'''
|
||||
minio bucket: 'jenkins', credentialsId: 'jenkins-minio', excludes: '', host: 'minio.weaselab.dev', includes: 'paper/*.pdf', targetFolder: '${JOB_NAME}/${BUILD_NUMBER}/${STAGE_NAME}/'
|
||||
minio bucket: 'jenkins', credentialsId: 'jenkins-minio', excludes: '', host: 'minio.weaselab.dev', includes: 'build/*.deb,build/*.rpm,paper/*.pdf', targetFolder: '${JOB_NAME}/${BUILD_NUMBER}/${STAGE_NAME}/'
|
||||
}
|
||||
}
|
||||
stage('Release [gcc,aarch64]') {
|
||||
agent {
|
||||
dockerfile {
|
||||
args '-v /home/jenkins/ccache:/ccache'
|
||||
reuseNode true
|
||||
}
|
||||
}
|
||||
steps {
|
||||
CleanBuildAndTest("-DCMAKE_TOOLCHAIN_FILE=../aarch64-toolchain.cmake -DCMAKE_CXX_FLAGS=-DNVALGRIND")
|
||||
sh '''
|
||||
cd build
|
||||
cpack -G DEB
|
||||
cpack -G RPM
|
||||
'''
|
||||
minio bucket: 'jenkins', credentialsId: 'jenkins-minio', excludes: '', host: 'minio.weaselab.dev', includes: 'build/*.deb,build/*.rpm', targetFolder: '${JOB_NAME}/${BUILD_NUMBER}/${STAGE_NAME}'
|
||||
}
|
||||
}
|
||||
stage('Coverage') {
|
||||
@@ -66,9 +106,9 @@ pipeline {
|
||||
}
|
||||
}
|
||||
steps {
|
||||
CleanBuildAndTest("-DCMAKE_C_FLAGS=--coverage -DCMAKE_CXX_FLAGS=--coverage -DCMAKE_BUILD_TYPE=Debug")
|
||||
CleanBuildAndTest("-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++ -DCMAKE_C_FLAGS=--coverage -DCMAKE_CXX_FLAGS=--coverage -DCMAKE_BUILD_TYPE=Debug")
|
||||
sh '''
|
||||
gcovr --gcov-executable "llvm-cov-15 gcov" --exclude '.*third_party.*' --cobertura > build/coverage.xml
|
||||
gcovr -f ConflictSet.cpp --cobertura > build/coverage.xml
|
||||
'''
|
||||
cobertura autoUpdateHealth: false, autoUpdateStability: false, coberturaReportFile: 'build/coverage.xml', conditionalCoverageTargets: '70, 0, 0', failUnhealthy: false, failUnstable: false, lineCoverageTargets: '80, 0, 0', maxNumberOfBuilds: 0, methodCoverageTargets: '80, 0, 0', onlyStable: false, sourceEncoding: 'ASCII', zoomCoverageChart: false
|
||||
}
|
||||
|
131
README.md
131
README.md
@@ -2,69 +2,104 @@ A data structure for optimistic concurrency control on ranges of bitwise-lexicog
|
||||
|
||||
Intended to replace FoundationDB's skip list.
|
||||
|
||||
Hardware for all benchmarks is a mac m1 2020.
|
||||
|
||||
# FoundationDB's benchmark
|
||||
|
||||
## Skip list
|
||||
|
||||
```
|
||||
New conflict set: 4.189 sec
|
||||
0.298 Mtransactions/sec
|
||||
1.194 Mkeys/sec
|
||||
Detect only: 3.990 sec
|
||||
0.313 Mtransactions/sec
|
||||
1.253 Mkeys/sec
|
||||
Skiplist only: 2.849 sec
|
||||
0.439 Mtransactions/sec
|
||||
1.755 Mkeys/sec
|
||||
New conflict set: 1.957 sec
|
||||
0.639 Mtransactions/sec
|
||||
2.555 Mkeys/sec
|
||||
Detect only: 1.845 sec
|
||||
0.678 Mtransactions/sec
|
||||
2.710 Mkeys/sec
|
||||
Skiplist only: 1.263 sec
|
||||
0.990 Mtransactions/sec
|
||||
3.960 Mkeys/sec
|
||||
Performance counters:
|
||||
Build: 0.0913
|
||||
Add: 0.0998
|
||||
Detect: 3.99
|
||||
D.Sort: 0.808
|
||||
D.Combine: 0.0309
|
||||
D.CheckRead: 1.67
|
||||
D.CheckIntraBatch: 0.0305
|
||||
D.MergeWrite: 1.18
|
||||
D.RemoveBefore: 0.265
|
||||
Build: 0.0546
|
||||
Add: 0.0563
|
||||
Detect: 1.84
|
||||
D.Sort: 0.412
|
||||
D.Combine: 0.0141
|
||||
D.CheckRead: 0.671
|
||||
D.CheckIntraBatch: 0.0068
|
||||
D.MergeWrite: 0.592
|
||||
D.RemoveBefore: 0.146
|
||||
```
|
||||
|
||||
## Radix tree (this implementation)
|
||||
|
||||
```
|
||||
New conflict set: 2.965 sec
|
||||
0.422 Mtransactions/sec
|
||||
1.686 Mkeys/sec
|
||||
Detect only: 2.761 sec
|
||||
0.453 Mtransactions/sec
|
||||
1.811 Mkeys/sec
|
||||
Skiplist only: 1.580 sec
|
||||
0.791 Mtransactions/sec
|
||||
3.165 Mkeys/sec
|
||||
New conflict set: 1.366 sec
|
||||
0.915 Mtransactions/sec
|
||||
3.660 Mkeys/sec
|
||||
Detect only: 1.248 sec
|
||||
1.002 Mtransactions/sec
|
||||
4.007 Mkeys/sec
|
||||
Skiplist only: 0.573 sec
|
||||
2.182 Mtransactions/sec
|
||||
8.730 Mkeys/sec
|
||||
Performance counters:
|
||||
Build: 0.0902
|
||||
Add: 0.107
|
||||
Detect: 2.76
|
||||
D.Sort: 0.809
|
||||
D.Combine: 0.0309
|
||||
D.CheckRead: 0.658
|
||||
D.CheckIntraBatch: 0.0294
|
||||
D.MergeWrite: 0.921
|
||||
D.RemoveBefore: 0.305
|
||||
Build: 0.0594
|
||||
Add: 0.0572
|
||||
Detect: 1.25
|
||||
D.Sort: 0.418
|
||||
D.Combine: 0.0149
|
||||
D.CheckRead: 0.232
|
||||
D.CheckIntraBatch: 0.0067
|
||||
D.MergeWrite: 0.341
|
||||
D.RemoveBefore: 0.232
|
||||
```
|
||||
|
||||
# Our benchmark
|
||||
|
||||
## Skip list
|
||||
|
||||
| ns/op | op/s | err% | total | benchmark
|
||||
|--------------------:|--------------------:|--------:|----------:|:----------
|
||||
| 263.10 | 3,800,889.33 | 1.6% | 0.64 | `skip list (point reads)`
|
||||
| 256.36 | 3,900,800.96 | 1.7% | 0.61 | `skip list (prefix reads)`
|
||||
| 526.95 | 1,897,714.85 | 1.6% | 1.23 | `skip list (range reads)`
|
||||
| 546.54 | 1,829,676.97 | 0.5% | 1.31 | `skip list (point writes)`
|
||||
| 539.23 | 1,854,511.73 | 0.5% | 1.29 | `skip list (prefix writes)`
|
||||
| 266.68 | 3,749,799.99 | 0.6% | 0.65 | `skip list (range writes)`
|
||||
| 18.31 | 54,612,706.60 | 0.4% | 0.04 | `radix tree (point reads)`
|
||||
| 48.78 | 20,498,870.77 | 0.2% | 0.12 | `radix tree (prefix reads)`
|
||||
| 347.16 | 2,880,474.86 | 0.9% | 0.83 | `radix tree (range reads)`
|
||||
| 34.80 | 28,734,706.67 | 3.6% | 0.09 | `radix tree (point writes)`
|
||||
| 70.34 | 14,216,970.16 | 1.4% | 0.17 | `radix tree (prefix writes)`
|
||||
| 82.41 | 12,134,555.86 | 1.0% | 0.20 | `radix tree (range writes)`
|
||||
| 246.99 | 4,048,700.59 | 0.2% | 0.01 | `point reads`
|
||||
| 260.16 | 3,843,784.65 | 0.1% | 0.01 | `prefix reads`
|
||||
| 493.35 | 2,026,953.19 | 0.1% | 0.01 | `range reads`
|
||||
| 462.05 | 2,164,289.23 | 0.6% | 0.01 | `point writes`
|
||||
| 448.19 | 2,231,205.25 | 0.9% | 0.01 | `prefix writes`
|
||||
| 255.83 | 3,908,845.72 | 1.5% | 0.02 | `range writes`
|
||||
| 582.63 | 1,716,349.02 | 1.3% | 0.01 | `monotonic increasing point writes`
|
||||
|
||||
## Radix tree (this implementation)
|
||||
|
||||
| ns/op | op/s | err% | total | benchmark
|
||||
|--------------------:|--------------------:|--------:|----------:|:----------
|
||||
| 19.42 | 51,483,206.67 | 0.3% | 0.01 | `point reads`
|
||||
| 58.43 | 17,115,612.57 | 0.1% | 0.01 | `prefix reads`
|
||||
| 216.09 | 4,627,766.60 | 0.2% | 0.01 | `range reads`
|
||||
| 28.35 | 35,267,567.72 | 0.2% | 0.01 | `point writes`
|
||||
| 43.43 | 23,026,226.17 | 0.2% | 0.01 | `prefix writes`
|
||||
| 50.00 | 20,000,000.00 | 0.0% | 0.01 | `range writes`
|
||||
| 92.38 | 10,824,863.69 | 4.1% | 0.01 | `monotonic increasing point writes`
|
||||
|
||||
# "Real data" test
|
||||
|
||||
Point queries only, best of three runs. Gc ratio is the ratio of time spent doing garbage collection to time spent adding writes or doing garbage collection. Lower is better.
|
||||
|
||||
## skip list
|
||||
|
||||
```
|
||||
Check: 11.3385 seconds, 329.718 MB/s, Add: 5.35612 seconds, 131.072 MB/s, Gc ratio: 45.7173%
|
||||
```
|
||||
|
||||
## radix tree
|
||||
|
||||
```
|
||||
Check: 2.48583 seconds, 1503.93 MB/s, Add: 2.12768 seconds, 329.954 MB/s, Gc ratio: 41.7943%
|
||||
```
|
||||
|
||||
## hash table
|
||||
|
||||
(The hash table implementation doesn't work on range queries, and its purpose is to provide an idea of how fast point queries can be)
|
||||
|
||||
```
|
||||
Check: 1.83386 seconds, 2038.6 MB/s, Add: 0.601411 seconds, 1167.32 MB/s, Gc ratio: 48.9776%
|
||||
```
|
||||
|
134
RealDataBench.cpp
Normal file
134
RealDataBench.cpp
Normal file
@@ -0,0 +1,134 @@
|
||||
#include <ConflictSet.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <fcntl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <vector>
|
||||
|
||||
using namespace weaselab;
|
||||
|
||||
double now() {
|
||||
return std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch())
|
||||
.count() *
|
||||
1e-9;
|
||||
}
|
||||
|
||||
inline size_t getPageSize() {
|
||||
static size_t kPageSize = sysconf(_SC_PAGESIZE);
|
||||
return kPageSize;
|
||||
}
|
||||
|
||||
/// Helper for rounding up to page size (or some other alignment)
|
||||
constexpr inline size_t rightAlign(size_t offset, size_t alignment) {
|
||||
return offset % alignment == 0 ? offset
|
||||
: ((offset / alignment) + 1) * alignment;
|
||||
}
|
||||
|
||||
int main(int argc, const char **argv) {
|
||||
// Use with this dataset https://snap.stanford.edu/data/memetracker9.html
|
||||
// Preprocess the files with `sed -i'' '/^Q/d'`
|
||||
|
||||
double checkTime = 0;
|
||||
double addTime = 0;
|
||||
double gcTime = 0;
|
||||
double checkBytes = 0;
|
||||
double addBytes = 0;
|
||||
|
||||
ConflictSet cs{0};
|
||||
int64_t version = 0;
|
||||
double timer = 0;
|
||||
|
||||
int64_t peakMemory = 0;
|
||||
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
int fd = open(argv[i], O_RDONLY);
|
||||
struct stat st;
|
||||
if (fstat(fd, &st) == -1) {
|
||||
int err = errno;
|
||||
fprintf(stderr, "stat error %s - %s\n", argv[i], strerror(err));
|
||||
fflush(stderr);
|
||||
abort();
|
||||
}
|
||||
|
||||
int64_t size = rightAlign(st.st_size, getPageSize());
|
||||
const uint8_t *begin =
|
||||
(uint8_t *)mmap(0, size, PROT_READ, MAP_PRIVATE, fd, 0);
|
||||
madvise((void *)begin, size, MADV_SEQUENTIAL);
|
||||
auto *const mapOriginal = begin;
|
||||
const auto sizeOriginal = size;
|
||||
|
||||
using StringView = std::basic_string_view<uint8_t>;
|
||||
|
||||
StringView write;
|
||||
std::vector<StringView> reads;
|
||||
std::vector<ConflictSet::ReadRange> readRanges;
|
||||
std::vector<ConflictSet::Result> results;
|
||||
|
||||
for (uint8_t *end = (uint8_t *)memchr(begin, '\n', size); end != nullptr;) {
|
||||
StringView line{begin, static_cast<size_t>(end - begin)};
|
||||
size -= end - begin + 1;
|
||||
begin = end + 1;
|
||||
end = (uint8_t *)memchr(begin, '\n', size);
|
||||
|
||||
if (line.size() > 0 && line[0] == 'P') {
|
||||
write = line.substr(2, line.size());
|
||||
} else if (line.size() > 0 && line[0] == 'L') {
|
||||
reads.push_back(line.substr(2, line.size()));
|
||||
} else if (line.empty()) {
|
||||
{
|
||||
readRanges.resize(reads.size());
|
||||
auto iter = readRanges.begin();
|
||||
for (const auto &read : reads) {
|
||||
iter->begin.p = (const uint8_t *)read.data();
|
||||
iter->begin.len = read.size();
|
||||
checkBytes += read.size();
|
||||
iter->end.len = 0;
|
||||
iter->readVersion = version - 100;
|
||||
++iter;
|
||||
}
|
||||
}
|
||||
results.resize(readRanges.size());
|
||||
|
||||
timer = now();
|
||||
cs.check(readRanges.data(), results.data(), readRanges.size());
|
||||
checkTime += now() - timer;
|
||||
|
||||
// Add unconditionally so that the load doesn't actually depend on the
|
||||
// conflict rate
|
||||
ConflictSet::WriteRange w;
|
||||
w.begin.p = (const uint8_t *)write.data();
|
||||
w.begin.len = write.size();
|
||||
w.end.len = 0;
|
||||
|
||||
addBytes += write.size();
|
||||
|
||||
timer = now();
|
||||
cs.addWrites(&w, 1, ++version);
|
||||
addTime += now() - timer;
|
||||
|
||||
write = {};
|
||||
reads.clear();
|
||||
|
||||
if (cs.getBytes() > peakMemory) {
|
||||
peakMemory = cs.getBytes();
|
||||
}
|
||||
|
||||
timer = now();
|
||||
cs.setOldestVersion(version - 10000);
|
||||
gcTime += now() - timer;
|
||||
}
|
||||
}
|
||||
munmap((void *)mapOriginal, sizeOriginal);
|
||||
close(fd);
|
||||
}
|
||||
|
||||
printf("Check: %g seconds, %g MB/s, Add: %g seconds, %g MB/s, Gc ratio: "
|
||||
"%g%%, Peak idle memory: %g\n",
|
||||
checkTime, checkBytes / checkTime * 1e-6, addTime,
|
||||
addBytes / addTime * 1e-6, gcTime / (gcTime + addTime) * 1e2,
|
||||
double(peakMemory));
|
||||
}
|
155
ScriptTest.cpp
Normal file
155
ScriptTest.cpp
Normal file
@@ -0,0 +1,155 @@
|
||||
#include "Internal.h"
|
||||
#include <ConflictSet.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <fcntl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <vector>
|
||||
|
||||
inline size_t getPageSize() {
|
||||
static size_t kPageSize = sysconf(_SC_PAGESIZE);
|
||||
return kPageSize;
|
||||
}
|
||||
|
||||
/// Helper for rounding up to page size (or some other alignment)
|
||||
constexpr inline size_t rightAlign(size_t offset, size_t alignment) {
|
||||
return offset % alignment == 0 ? offset
|
||||
: ((offset / alignment) + 1) * alignment;
|
||||
}
|
||||
|
||||
using StringView = std::basic_string_view<uint8_t>;
|
||||
|
||||
inline StringView operator"" _v(const char *str, size_t size) {
|
||||
return {reinterpret_cast<const uint8_t *>(str), size};
|
||||
}
|
||||
|
||||
int main(int argc, const char **argv) {
|
||||
|
||||
ConflictSet cs{0};
|
||||
ReferenceImpl ref{0};
|
||||
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
int fd = open(argv[i], O_RDONLY);
|
||||
struct stat st;
|
||||
if (fstat(fd, &st) == -1) {
|
||||
int err = errno;
|
||||
fprintf(stderr, "stat error %s - %s\n", argv[i], strerror(err));
|
||||
fflush(stderr);
|
||||
abort();
|
||||
}
|
||||
|
||||
int64_t size = rightAlign(st.st_size, getPageSize());
|
||||
const uint8_t *begin =
|
||||
(uint8_t *)mmap(0, size, PROT_READ, MAP_PRIVATE, fd, 0);
|
||||
madvise((void *)begin, size, MADV_SEQUENTIAL);
|
||||
auto *const mapOriginal = begin;
|
||||
const auto sizeOriginal = size;
|
||||
|
||||
StringView b;
|
||||
StringView e;
|
||||
int64_t v = 0;
|
||||
int64_t lastWriteVersion = 0;
|
||||
int64_t lastOldestVersion = 0;
|
||||
std::vector<ConflictSet::WriteRange> writeRanges;
|
||||
std::vector<ConflictSet::ReadRange> readRanges;
|
||||
std::vector<ConflictSet::Result> results;
|
||||
|
||||
for (uint8_t *end = (uint8_t *)memchr(begin, '\n', size); end != nullptr;) {
|
||||
StringView line{begin, static_cast<size_t>(end - begin)};
|
||||
size -= end - begin + 1;
|
||||
begin = end + 1;
|
||||
end = (uint8_t *)memchr(begin, '\n', size);
|
||||
|
||||
if (line.starts_with("begin"_v)) {
|
||||
b = line.substr("begin "_v.size(), line.size());
|
||||
printf("b <- %.*s\n", int(b.size()), b.data());
|
||||
} else if (line.starts_with("end"_v)) {
|
||||
e = line.substr("end "_v.size(), line.size());
|
||||
printf("e <- %.*s\n", int(e.size()), e.data());
|
||||
} else if (line.starts_with("version"_v)) {
|
||||
line = line.substr("version "_v.size(), line.size());
|
||||
v = 0;
|
||||
for (auto c : line) {
|
||||
v = v * 10 + int(c) - int('0');
|
||||
}
|
||||
printf("v <- %" PRId64 "\n", v);
|
||||
} else if (line.starts_with("pointread"_v)) {
|
||||
printf("pointread\n");
|
||||
ConflictSet::ReadRange r;
|
||||
r.begin.p = b.data();
|
||||
r.begin.len = b.size();
|
||||
r.end.len = 0;
|
||||
r.readVersion = v;
|
||||
readRanges.push_back(r);
|
||||
} else if (line.starts_with("pointwrite"_v)) {
|
||||
printf("pointwrite\n");
|
||||
assert(writeRanges.empty() ||
|
||||
(writeRanges.back().end.len == 0 ? writeRanges.back().begin
|
||||
: writeRanges.back().end) < b);
|
||||
ConflictSet::WriteRange w;
|
||||
w.begin.p = b.data();
|
||||
w.begin.len = b.size();
|
||||
w.end.len = 0;
|
||||
writeRanges.push_back(w);
|
||||
} else if (line.starts_with("rangeread"_v)) {
|
||||
printf("rangeread\n");
|
||||
ConflictSet::ReadRange r;
|
||||
r.begin.p = b.data();
|
||||
r.begin.len = b.size();
|
||||
r.end.p = e.data();
|
||||
r.end.len = e.size();
|
||||
r.readVersion = v;
|
||||
readRanges.push_back(r);
|
||||
} else if (line.starts_with("rangewrite"_v)) {
|
||||
printf("rangewrite\n");
|
||||
assert(b < e);
|
||||
assert(writeRanges.empty() ||
|
||||
(writeRanges.back().end.len == 0 ? writeRanges.back().begin
|
||||
: writeRanges.back().end) < b);
|
||||
ConflictSet::WriteRange w;
|
||||
w.begin.p = b.data();
|
||||
w.begin.len = b.size();
|
||||
w.end.p = e.data();
|
||||
w.end.len = e.size();
|
||||
writeRanges.push_back(w);
|
||||
} else if (line.starts_with("check"_v)) {
|
||||
printf("check\n");
|
||||
Arena arena;
|
||||
auto *expected = new (arena) ConflictSet::Result[readRanges.size()];
|
||||
auto *actual = new (arena) ConflictSet::Result[readRanges.size()];
|
||||
ref.check(readRanges.data(), expected, readRanges.size());
|
||||
cs.check(readRanges.data(), actual, readRanges.size());
|
||||
for (int i = 0; i < int(readRanges.size()); ++i) {
|
||||
if (expected[i] != actual[i]) {
|
||||
fprintf(stderr, "Expected %s, got %s at index %d\n",
|
||||
resultToStr(expected[i]), resultToStr(actual[i]), i);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
readRanges = {};
|
||||
} else if (line.starts_with("addwrites"_v)) {
|
||||
printf("addwrites\n");
|
||||
assert(v > lastWriteVersion);
|
||||
lastWriteVersion = v;
|
||||
cs.addWrites(writeRanges.data(), writeRanges.size(), v);
|
||||
ref.addWrites(writeRanges.data(), writeRanges.size(), v);
|
||||
writeRanges = {};
|
||||
} else if (line.starts_with("setoldest"_v)) {
|
||||
printf("setoldest\n");
|
||||
assert(v > lastOldestVersion);
|
||||
lastOldestVersion = v;
|
||||
cs.setOldestVersion(v);
|
||||
ref.setOldestVersion(v);
|
||||
} else if (line.empty() || line.starts_with(";"_v)) {
|
||||
// skip
|
||||
} else {
|
||||
printf("Unrecognized line: %.*s\n", int(line.size()), line.data());
|
||||
}
|
||||
}
|
||||
munmap((void *)mapOriginal, sizeOriginal);
|
||||
close(fd);
|
||||
}
|
||||
}
|
735
SkipList.cpp
Normal file
735
SkipList.cpp
Normal file
@@ -0,0 +1,735 @@
|
||||
/*
|
||||
* SkipList.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.
|
||||
*/
|
||||
|
||||
#include "ConflictSet.h"
|
||||
#include "Internal.h"
|
||||
#include <span>
|
||||
|
||||
std::span<const uint8_t> keyAfter(Arena &arena, std::span<const uint8_t> key) {
|
||||
auto result =
|
||||
std::span<uint8_t>(new (arena) uint8_t[key.size() + 1], key.size() + 1);
|
||||
memcpy(result.data(), key.data(), key.size());
|
||||
result[result.size() - 1] = 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
std::span<const uint8_t> copyToArena(Arena &arena,
|
||||
std::span<const uint8_t> key) {
|
||||
auto result = std::span<uint8_t>(new (arena) uint8_t[key.size()], key.size());
|
||||
memcpy(result.data(), key.data(), key.size());
|
||||
return result;
|
||||
}
|
||||
|
||||
using Version = int64_t;
|
||||
#define force_inline __attribute__((always_inline))
|
||||
using StringRef = std::span<const uint8_t>;
|
||||
|
||||
struct KeyRangeRef {
|
||||
StringRef begin;
|
||||
StringRef end;
|
||||
KeyRangeRef() {}
|
||||
KeyRangeRef(StringRef begin, StringRef end) : begin(begin), end(end) {}
|
||||
KeyRangeRef(Arena &arena, StringRef begin)
|
||||
: begin(begin), end(keyAfter(arena, begin)) {}
|
||||
};
|
||||
|
||||
static thread_local uint32_t g_seed = 0;
|
||||
|
||||
static inline int skfastrand() {
|
||||
g_seed = g_seed * 1664525L + 1013904223L;
|
||||
return g_seed;
|
||||
}
|
||||
|
||||
static int compare(const StringRef &a, const StringRef &b) {
|
||||
int c = memcmp(a.data(), b.data(), std::min(a.size(), b.size()));
|
||||
if (c < 0)
|
||||
return -1;
|
||||
if (c > 0)
|
||||
return +1;
|
||||
if (a.size() < b.size())
|
||||
return -1;
|
||||
if (a.size() == b.size())
|
||||
return 0;
|
||||
return +1;
|
||||
}
|
||||
|
||||
struct ReadConflictRange {
|
||||
StringRef begin, end;
|
||||
Version version;
|
||||
|
||||
ReadConflictRange() {}
|
||||
ReadConflictRange(StringRef begin, StringRef end, Version version)
|
||||
: begin(begin), end(end), version(version) {}
|
||||
bool operator<(const ReadConflictRange &rhs) const {
|
||||
return compare(begin, rhs.begin) < 0;
|
||||
}
|
||||
};
|
||||
|
||||
class SkipList {
|
||||
private:
|
||||
static constexpr int MaxLevels = 26;
|
||||
|
||||
int randomLevel() const {
|
||||
uint32_t i = uint32_t(skfastrand()) >> (32 - (MaxLevels - 1));
|
||||
int level = 0;
|
||||
while (i & 1) {
|
||||
i >>= 1;
|
||||
level++;
|
||||
}
|
||||
assert(level < MaxLevels);
|
||||
return level;
|
||||
}
|
||||
|
||||
// Represent a node in the SkipList. The node has multiple (i.e., level)
|
||||
// pointers to other nodes, and keeps a record of the max versions for each
|
||||
// level.
|
||||
struct Node {
|
||||
int level() const { return nPointers - 1; }
|
||||
uint8_t *value() {
|
||||
return end() + nPointers * (sizeof(Node *) + sizeof(Version));
|
||||
}
|
||||
int length() const { return valueLength; }
|
||||
|
||||
// Returns the next node pointer at the given level.
|
||||
Node *getNext(int level) { return *((Node **)end() + level); }
|
||||
// Sets the next node pointer at the given level.
|
||||
void setNext(int level, Node *n) { *((Node **)end() + level) = n; }
|
||||
|
||||
// Returns the max version at the given level.
|
||||
Version getMaxVersion(int i) const {
|
||||
return ((Version *)(end() + nPointers * sizeof(Node *)))[i];
|
||||
}
|
||||
// Sets the max version at the given level.
|
||||
void setMaxVersion(int i, Version v) {
|
||||
((Version *)(end() + nPointers * sizeof(Node *)))[i] = v;
|
||||
}
|
||||
|
||||
// Return a node with initialized value but uninitialized pointers
|
||||
// Memory layout: *this, (level+1) Node*, (level+1) Version, value
|
||||
static Node *create(const StringRef &value, int level) {
|
||||
int nodeSize = sizeof(Node) + value.size() +
|
||||
(level + 1) * (sizeof(Node *) + sizeof(Version));
|
||||
|
||||
Node *n;
|
||||
n = (Node *)safe_malloc(nodeSize);
|
||||
|
||||
n->nPointers = level + 1;
|
||||
|
||||
n->valueLength = value.size();
|
||||
if (value.size() > 0) {
|
||||
memcpy(n->value(), value.data(), value.size());
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
// pre: level>0, all lower level nodes between this and getNext(level) have
|
||||
// correct maxversions
|
||||
void calcVersionForLevel(int level) {
|
||||
Node *end = getNext(level);
|
||||
Version v = getMaxVersion(level - 1);
|
||||
for (Node *x = getNext(level - 1); x != end; x = x->getNext(level - 1))
|
||||
v = std::max(v, x->getMaxVersion(level - 1));
|
||||
setMaxVersion(level, v);
|
||||
}
|
||||
|
||||
void destroy() { safe_free(this, getNodeSize()); }
|
||||
|
||||
private:
|
||||
int getNodeSize() const {
|
||||
return sizeof(Node) + valueLength +
|
||||
nPointers * (sizeof(Node *) + sizeof(Version));
|
||||
}
|
||||
// Returns the first Node* pointer
|
||||
uint8_t *end() { return (uint8_t *)(this + 1); }
|
||||
uint8_t const *end() const { return (uint8_t const *)(this + 1); }
|
||||
int nPointers, valueLength;
|
||||
};
|
||||
|
||||
static force_inline bool less(const uint8_t *a, int aLen, const uint8_t *b,
|
||||
int bLen) {
|
||||
int c = memcmp(a, b, std::min(aLen, bLen));
|
||||
if (c < 0)
|
||||
return true;
|
||||
if (c > 0)
|
||||
return false;
|
||||
return aLen < bLen;
|
||||
}
|
||||
|
||||
Node *header;
|
||||
|
||||
void destroy() {
|
||||
Node *next, *x;
|
||||
for (x = header; x; x = next) {
|
||||
next = x->getNext(0);
|
||||
x->destroy();
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
// Points the location (i.e., Node*) that value would appear in the SkipList.
|
||||
// If the "value" is in the list, then finger[0] points to that exact node;
|
||||
// otherwise, the finger points to Nodes that the value should be inserted
|
||||
// before. Note the SkipList organizes all nodes at level 0, higher levels
|
||||
// contain jump pointers.
|
||||
struct Finger {
|
||||
Node *finger[MaxLevels]; // valid for levels >= level
|
||||
int level = MaxLevels;
|
||||
Node *x = nullptr;
|
||||
Node *alreadyChecked = nullptr;
|
||||
StringRef value;
|
||||
|
||||
Finger() = default;
|
||||
Finger(Node *header, const StringRef &ptr) : x(header), value(ptr) {}
|
||||
|
||||
void init(const StringRef &value, Node *header) {
|
||||
this->value = value;
|
||||
x = header;
|
||||
alreadyChecked = nullptr;
|
||||
level = MaxLevels;
|
||||
}
|
||||
|
||||
// pre: !finished()
|
||||
force_inline void prefetch() {
|
||||
Node *next = x->getNext(0);
|
||||
__builtin_prefetch(next);
|
||||
}
|
||||
|
||||
// pre: !finished()
|
||||
// Advances the pointer at the current level to a Node that's >= finger's
|
||||
// value if possible; or move to the next level (i.e., level--). Returns
|
||||
// true if we have advanced to the next level
|
||||
force_inline bool advance() {
|
||||
Node *next = x->getNext(level - 1);
|
||||
|
||||
if (next == alreadyChecked ||
|
||||
!less(next->value(), next->length(), value.data(), value.size())) {
|
||||
alreadyChecked = next;
|
||||
level--;
|
||||
finger[level] = x;
|
||||
return true;
|
||||
} else {
|
||||
x = next;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// pre: !finished()
|
||||
force_inline void nextLevel() {
|
||||
while (!advance())
|
||||
;
|
||||
}
|
||||
|
||||
force_inline bool finished() const { return level == 0; }
|
||||
|
||||
// Returns if the finger value is found in the SkipList.
|
||||
force_inline Node *found() const {
|
||||
// valid after finished returns true
|
||||
Node *n = finger[0]->getNext(
|
||||
0); // or alreadyChecked, but that is more easily invalidated
|
||||
if (n && n->length() == value.size() &&
|
||||
!memcmp(n->value(), value.data(), value.size()))
|
||||
return n;
|
||||
else
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
StringRef getValue() const {
|
||||
Node *n = finger[0]->getNext(0);
|
||||
return n ? StringRef(n->value(), n->length()) : StringRef();
|
||||
}
|
||||
};
|
||||
|
||||
// Returns the total number of nodes in the list.
|
||||
int count() const {
|
||||
int count = 0;
|
||||
Node *x = header->getNext(0);
|
||||
while (x) {
|
||||
x = x->getNext(0);
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
explicit SkipList(Version version = 0) {
|
||||
header = Node::create(StringRef(), MaxLevels - 1);
|
||||
for (int l = 0; l < MaxLevels; l++) {
|
||||
header->setNext(l, nullptr);
|
||||
header->setMaxVersion(l, version);
|
||||
}
|
||||
}
|
||||
~SkipList() { destroy(); }
|
||||
SkipList(SkipList &&other) noexcept : header(other.header) {
|
||||
other.header = nullptr;
|
||||
}
|
||||
void operator=(SkipList &&other) noexcept {
|
||||
destroy();
|
||||
header = other.header;
|
||||
other.header = nullptr;
|
||||
}
|
||||
void swap(SkipList &other) { std::swap(header, other.header); }
|
||||
|
||||
void addConflictRanges(const Finger *fingers, int rangeCount,
|
||||
Version version) {
|
||||
for (int r = rangeCount - 1; r >= 0; r--) {
|
||||
const Finger &startF = fingers[r * 2];
|
||||
const Finger &endF = fingers[r * 2 + 1];
|
||||
|
||||
if (endF.found() == nullptr)
|
||||
insert(endF, endF.finger[0]->getMaxVersion(0));
|
||||
|
||||
remove(startF, endF);
|
||||
insert(startF, version);
|
||||
}
|
||||
}
|
||||
|
||||
void detectConflicts(ReadConflictRange *ranges, int count,
|
||||
ConflictSet::Result *transactionConflictStatus) const {
|
||||
const int M = 16;
|
||||
int nextJob[M];
|
||||
CheckMax inProgress[M];
|
||||
if (!count)
|
||||
return;
|
||||
|
||||
int started = std::min(M, count);
|
||||
for (int i = 0; i < started; i++) {
|
||||
inProgress[i].init(ranges[i], header, transactionConflictStatus + i);
|
||||
nextJob[i] = i + 1;
|
||||
}
|
||||
nextJob[started - 1] = 0;
|
||||
|
||||
int prevJob = started - 1;
|
||||
int job = 0;
|
||||
// vtune: 340 parts
|
||||
while (true) {
|
||||
if (inProgress[job].advance()) {
|
||||
if (started == count) {
|
||||
if (prevJob == job)
|
||||
break;
|
||||
nextJob[prevJob] = nextJob[job];
|
||||
job = prevJob;
|
||||
} else {
|
||||
int temp = started++;
|
||||
inProgress[job].init(ranges[temp], header,
|
||||
transactionConflictStatus + temp);
|
||||
}
|
||||
}
|
||||
prevJob = job;
|
||||
job = nextJob[job];
|
||||
}
|
||||
}
|
||||
|
||||
void find(const StringRef *values, Finger *results, int *temp, int count) {
|
||||
// Relying on the ordering of values, descend until the values aren't all in
|
||||
// the same part of the tree
|
||||
|
||||
// vtune: 11 parts
|
||||
results[0].init(values[0], header);
|
||||
const StringRef &endValue = values[count - 1];
|
||||
while (results[0].level > 1) {
|
||||
results[0].nextLevel();
|
||||
Node *ac = results[0].alreadyChecked;
|
||||
if (ac &&
|
||||
less(ac->value(), ac->length(), endValue.data(), endValue.size()))
|
||||
break;
|
||||
}
|
||||
|
||||
// Init all the other fingers to start descending where we stopped
|
||||
// the first one
|
||||
|
||||
// SOMEDAY: this loop showed up on vtune, could be faster?
|
||||
// vtune: 8 parts
|
||||
int startLevel = results[0].level + 1;
|
||||
Node *x = startLevel < MaxLevels ? results[0].finger[startLevel] : header;
|
||||
for (int i = 1; i < count; i++) {
|
||||
results[i].level = startLevel;
|
||||
results[i].x = x;
|
||||
results[i].alreadyChecked = nullptr;
|
||||
results[i].value = values[i];
|
||||
for (int j = startLevel; j < MaxLevels; j++)
|
||||
results[i].finger[j] = results[0].finger[j];
|
||||
}
|
||||
|
||||
int *nextJob = temp;
|
||||
for (int i = 0; i < count - 1; i++)
|
||||
nextJob[i] = i + 1;
|
||||
nextJob[count - 1] = 0;
|
||||
|
||||
int prevJob = count - 1;
|
||||
int job = 0;
|
||||
|
||||
// vtune: 225 parts
|
||||
while (true) {
|
||||
Finger *f = &results[job];
|
||||
f->advance();
|
||||
if (f->finished()) {
|
||||
if (prevJob == job)
|
||||
break;
|
||||
nextJob[prevJob] = nextJob[job];
|
||||
} else {
|
||||
f->prefetch();
|
||||
prevJob = job;
|
||||
}
|
||||
job = nextJob[job];
|
||||
}
|
||||
}
|
||||
|
||||
int removeBefore(Version v, Finger &f, int nodeCount) {
|
||||
// f.x, f.alreadyChecked?
|
||||
|
||||
int removedCount = 0;
|
||||
bool wasAbove = true;
|
||||
while (nodeCount--) {
|
||||
Node *x = f.finger[0]->getNext(0);
|
||||
if (!x)
|
||||
break;
|
||||
|
||||
// double prefetch gives +25% speed (single threaded)
|
||||
Node *next = x->getNext(0);
|
||||
__builtin_prefetch(next);
|
||||
next = x->getNext(1);
|
||||
__builtin_prefetch(next);
|
||||
|
||||
bool isAbove = x->getMaxVersion(0) >= v;
|
||||
if (isAbove || wasAbove) { // f.nextItem
|
||||
for (int l = 0; l <= x->level(); l++)
|
||||
f.finger[l] = x;
|
||||
} else { // f.eraseItem
|
||||
removedCount++;
|
||||
for (int l = 0; l <= x->level(); l++)
|
||||
f.finger[l]->setNext(l, x->getNext(l));
|
||||
for (int i = 1; i <= x->level(); i++)
|
||||
f.finger[i]->setMaxVersion(
|
||||
i, std::max(f.finger[i]->getMaxVersion(i), x->getMaxVersion(i)));
|
||||
x->destroy();
|
||||
}
|
||||
wasAbove = isAbove;
|
||||
}
|
||||
|
||||
return removedCount;
|
||||
}
|
||||
|
||||
private:
|
||||
void remove(const Finger &start, const Finger &end) {
|
||||
if (start.finger[0] == end.finger[0])
|
||||
return;
|
||||
|
||||
Node *x = start.finger[0]->getNext(0);
|
||||
|
||||
// vtune says: this loop is the expensive parts (6 parts)
|
||||
for (int i = 0; i < MaxLevels; i++)
|
||||
if (start.finger[i] != end.finger[i])
|
||||
start.finger[i]->setNext(i, end.finger[i]->getNext(i));
|
||||
|
||||
while (true) {
|
||||
Node *next = x->getNext(0);
|
||||
x->destroy();
|
||||
if (x == end.finger[0])
|
||||
break;
|
||||
x = next;
|
||||
}
|
||||
}
|
||||
|
||||
void insert(const Finger &f, Version version) {
|
||||
int level = randomLevel();
|
||||
// std::cout << std::string((const char*)value,length) << " level: " <<
|
||||
// level << std::endl;
|
||||
Node *x = Node::create(f.value, level);
|
||||
x->setMaxVersion(0, version);
|
||||
for (int i = 0; i <= level; i++) {
|
||||
x->setNext(i, f.finger[i]->getNext(i));
|
||||
f.finger[i]->setNext(i, x);
|
||||
}
|
||||
// vtune says: this loop is the costly part of this function
|
||||
for (int i = 1; i <= level; i++) {
|
||||
f.finger[i]->calcVersionForLevel(i);
|
||||
x->calcVersionForLevel(i);
|
||||
}
|
||||
for (int i = level + 1; i < MaxLevels; i++) {
|
||||
Version v = f.finger[i]->getMaxVersion(i);
|
||||
if (v >= version)
|
||||
break;
|
||||
f.finger[i]->setMaxVersion(i, version);
|
||||
}
|
||||
}
|
||||
|
||||
struct CheckMax {
|
||||
Finger start, end;
|
||||
Version version;
|
||||
ConflictSet::Result *result;
|
||||
int state;
|
||||
|
||||
void init(const ReadConflictRange &r, Node *header,
|
||||
ConflictSet::Result *result) {
|
||||
this->start.init(r.begin, header);
|
||||
this->end.init(r.end, header);
|
||||
this->version = r.version;
|
||||
this->state = 0;
|
||||
this->result = result;
|
||||
}
|
||||
|
||||
bool noConflict() const { return true; }
|
||||
bool conflict() {
|
||||
*result = ConflictSet::Conflict;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Return true if finished
|
||||
force_inline bool advance() {
|
||||
if (*result == ConflictSet::TooOld) {
|
||||
return true;
|
||||
}
|
||||
switch (state) {
|
||||
case 0:
|
||||
// find where start and end fingers diverge
|
||||
while (true) {
|
||||
if (!start.advance()) {
|
||||
start.prefetch();
|
||||
return false;
|
||||
}
|
||||
end.x = start.x;
|
||||
while (!end.advance())
|
||||
;
|
||||
|
||||
int l = start.level;
|
||||
if (start.finger[l] != end.finger[l])
|
||||
break;
|
||||
// accept if the range spans the check range, but does not have a
|
||||
// greater version
|
||||
if (start.finger[l]->getMaxVersion(l) <= version)
|
||||
return noConflict();
|
||||
if (l == 0)
|
||||
return conflict();
|
||||
}
|
||||
state = 1;
|
||||
case 1: {
|
||||
// check the end side of the pyramid
|
||||
Node *e = end.finger[end.level];
|
||||
while (e->getMaxVersion(end.level) > version) {
|
||||
if (end.finished())
|
||||
return conflict();
|
||||
end.nextLevel();
|
||||
Node *f = end.finger[end.level];
|
||||
while (e != f) {
|
||||
if (e->getMaxVersion(end.level) > version)
|
||||
return conflict();
|
||||
e = e->getNext(end.level);
|
||||
}
|
||||
}
|
||||
|
||||
// check the start side of the pyramid
|
||||
Node *s = end.finger[start.level];
|
||||
while (true) {
|
||||
Node *nextS = start.finger[start.level]->getNext(start.level);
|
||||
Node *p = nextS;
|
||||
while (p != s) {
|
||||
if (p->getMaxVersion(start.level) > version)
|
||||
return conflict();
|
||||
p = p->getNext(start.level);
|
||||
}
|
||||
if (start.finger[start.level]->getMaxVersion(start.level) <= version)
|
||||
return noConflict();
|
||||
s = nextS;
|
||||
if (start.finished()) {
|
||||
if (nextS->length() == start.value.size() &&
|
||||
!memcmp(nextS->value(), start.value.data(), start.value.size()))
|
||||
return noConflict();
|
||||
else
|
||||
return conflict();
|
||||
}
|
||||
start.nextLevel();
|
||||
}
|
||||
}
|
||||
default:
|
||||
__builtin_unreachable();
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
struct SkipListConflictSet {};
|
||||
|
||||
struct __attribute__((visibility("hidden"))) ConflictSet::Impl {
|
||||
Impl(int64_t oldestVersion)
|
||||
: oldestVersion(oldestVersion), skipList(oldestVersion) {}
|
||||
void check(const ConflictSet::ReadRange *reads, ConflictSet::Result *results,
|
||||
int count) const {
|
||||
Arena arena;
|
||||
auto *ranges = new (arena) ReadConflictRange[count];
|
||||
for (int i = 0; i < count; ++i) {
|
||||
ranges[i].begin = {reads[i].begin.p, size_t(reads[i].begin.len)};
|
||||
ranges[i].end = reads[i].end.len > 0
|
||||
? StringRef{reads[i].end.p, size_t(reads[i].end.len)}
|
||||
: keyAfter(arena, ranges[i].begin);
|
||||
ranges[i].version = reads[i].readVersion;
|
||||
results[i] = ConflictSet::Commit;
|
||||
}
|
||||
skipList.detectConflicts(ranges, count, results);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
if (reads[i].readVersion < oldestVersion) {
|
||||
results[i] = TooOld;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void addWrites(const ConflictSet::WriteRange *writes, int count,
|
||||
int64_t writeVersion) {
|
||||
Arena arena;
|
||||
const int stringCount = count * 2;
|
||||
|
||||
const int stripeSize = 16;
|
||||
SkipList::Finger fingers[stripeSize];
|
||||
int temp[stripeSize];
|
||||
int stripes = (stringCount + stripeSize - 1) / stripeSize;
|
||||
StringRef values[stripeSize];
|
||||
int64_t writeVersions[stripeSize / 2];
|
||||
int ss = stringCount - (stripes - 1) * stripeSize;
|
||||
for (int s = stripes - 1; s >= 0; s--) {
|
||||
for (int i = 0; i * 2 < ss; ++i) {
|
||||
const auto &w = writes[s * stripeSize / 2 + i];
|
||||
values[i * 2] = {w.begin.p, size_t(w.begin.len)};
|
||||
values[i * 2 + 1] = w.end.len > 0
|
||||
? StringRef{w.end.p, size_t(w.end.len)}
|
||||
: keyAfter(arena, values[i * 2]);
|
||||
keyUpdates += 3;
|
||||
}
|
||||
skipList.find(values, fingers, temp, ss);
|
||||
skipList.addConflictRanges(fingers, ss / 2, writeVersion);
|
||||
ss = stripeSize;
|
||||
}
|
||||
}
|
||||
|
||||
void setOldestVersion(int64_t oldestVersion) {
|
||||
this->oldestVersion = oldestVersion;
|
||||
SkipList::Finger finger;
|
||||
int temp;
|
||||
std::span<const uint8_t> key = removalKey;
|
||||
skipList.find(&key, &finger, &temp, 1);
|
||||
skipList.removeBefore(oldestVersion, finger, std::exchange(keyUpdates, 10));
|
||||
removalArena = Arena();
|
||||
removalKey = copyToArena(
|
||||
removalArena, {finger.getValue().data(), finger.getValue().size()});
|
||||
}
|
||||
|
||||
int64_t totalBytes = 0;
|
||||
|
||||
private:
|
||||
int64_t keyUpdates = 10;
|
||||
Arena removalArena;
|
||||
std::span<const uint8_t> removalKey;
|
||||
int64_t oldestVersion;
|
||||
SkipList skipList;
|
||||
};
|
||||
|
||||
void ConflictSet::check(const ReadRange *reads, Result *results,
|
||||
int count) const {
|
||||
impl->check(reads, results, count);
|
||||
}
|
||||
|
||||
void ConflictSet::addWrites(const WriteRange *writes, int count,
|
||||
int64_t writeVersion) {
|
||||
mallocBytesDelta = 0;
|
||||
impl->addWrites(writes, count, writeVersion);
|
||||
impl->totalBytes += mallocBytesDelta;
|
||||
#if SHOW_MEMORY
|
||||
if (impl->totalBytes != mallocBytes) {
|
||||
abort();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void ConflictSet::setOldestVersion(int64_t oldestVersion) {
|
||||
mallocBytesDelta = 0;
|
||||
impl->setOldestVersion(oldestVersion);
|
||||
impl->totalBytes += mallocBytesDelta;
|
||||
#if SHOW_MEMORY
|
||||
if (impl->totalBytes != mallocBytes) {
|
||||
abort();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
int64_t ConflictSet::getBytes() const { return impl->totalBytes; }
|
||||
|
||||
ConflictSet::ConflictSet(int64_t oldestVersion)
|
||||
: impl((mallocBytesDelta = 0,
|
||||
new (safe_malloc(sizeof(Impl))) Impl{oldestVersion})) {
|
||||
impl->totalBytes += mallocBytesDelta;
|
||||
}
|
||||
|
||||
ConflictSet::~ConflictSet() {
|
||||
if (impl) {
|
||||
impl->~Impl();
|
||||
safe_free(impl, sizeof(Impl));
|
||||
}
|
||||
}
|
||||
|
||||
ConflictSet::ConflictSet(ConflictSet &&other) noexcept
|
||||
: impl(std::exchange(other.impl, nullptr)) {}
|
||||
|
||||
ConflictSet &ConflictSet::operator=(ConflictSet &&other) noexcept {
|
||||
impl = std::exchange(other.impl, nullptr);
|
||||
return *this;
|
||||
}
|
||||
|
||||
using ConflictSet_Result = ConflictSet::Result;
|
||||
using ConflictSet_Key = ConflictSet::Key;
|
||||
using ConflictSet_ReadRange = ConflictSet::ReadRange;
|
||||
using ConflictSet_WriteRange = ConflictSet::WriteRange;
|
||||
|
||||
extern "C" {
|
||||
__attribute__((__visibility__("default"))) void
|
||||
ConflictSet_check(void *cs, const ConflictSet_ReadRange *reads,
|
||||
ConflictSet_Result *results, int count) {
|
||||
((ConflictSet::Impl *)cs)->check(reads, results, count);
|
||||
}
|
||||
__attribute__((__visibility__("default"))) void
|
||||
ConflictSet_addWrites(void *cs, const ConflictSet_WriteRange *writes, int count,
|
||||
int64_t writeVersion) {
|
||||
((ConflictSet::Impl *)cs)->addWrites(writes, count, writeVersion);
|
||||
}
|
||||
__attribute__((__visibility__("default"))) void
|
||||
ConflictSet_setOldestVersion(void *cs, int64_t oldestVersion) {
|
||||
((ConflictSet::Impl *)cs)->setOldestVersion(oldestVersion);
|
||||
}
|
||||
__attribute__((__visibility__("default"))) void *
|
||||
ConflictSet_create(int64_t oldestVersion) {
|
||||
return new (safe_malloc(sizeof(ConflictSet::Impl)))
|
||||
ConflictSet::Impl{oldestVersion};
|
||||
}
|
||||
__attribute__((__visibility__("default"))) void ConflictSet_destroy(void *cs) {
|
||||
using Impl = ConflictSet::Impl;
|
||||
((Impl *)cs)->~Impl();
|
||||
safe_free(cs, sizeof(Impl));
|
||||
}
|
||||
__attribute__((__visibility__("default"))) int64_t
|
||||
ConflictSet_getBytes(void *cs) {
|
||||
using Impl = ConflictSet::Impl;
|
||||
return ((Impl *)cs)->totalBytes;
|
||||
}
|
||||
}
|
||||
|
||||
#if SHOW_MEMORY
|
||||
struct __attribute__((visibility("default"))) PeakPrinter {
|
||||
~PeakPrinter() {
|
||||
printf("malloc bytes: %g\n", double(mallocBytes));
|
||||
printf("Peak malloc bytes: %g\n", double(peakMallocBytes));
|
||||
}
|
||||
} peakPrinter;
|
||||
#endif
|
@@ -5,7 +5,6 @@
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
printf("Running: %s\n", argv[i]);
|
||||
std::ifstream t(argv[i], std::ios::binary);
|
||||
std::stringstream buffer;
|
||||
buffer << t.rdbuf();
|
||||
|
7
aarch64-toolchain.cmake
Normal file
7
aarch64-toolchain.cmake
Normal file
@@ -0,0 +1,7 @@
|
||||
set(CMAKE_SYSTEM_NAME Linux)
|
||||
set(CMAKE_SYSTEM_PROCESSOR aarch64)
|
||||
set(CMAKE_C_COMPILER "/usr/bin/aarch64-linux-gnu-gcc")
|
||||
set(CMAKE_CXX_COMPILER "/usr/bin/aarch64-linux-gnu-g++")
|
||||
set(CMAKE_FIND_ROOT_PATH /usr/aarch64-linux-gnu)
|
||||
set(CMAKE_CROSSCOMPILING_EMULATOR "qemu-aarch64;-L;/usr/aarch64-linux-gnu/")
|
||||
set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE arm64)
|
@@ -7,17 +7,19 @@ int main(void) {
|
||||
ConflictSet_WriteRange w;
|
||||
ConflictSet_Result result;
|
||||
ConflictSet_ReadRange r;
|
||||
int64_t bytes;
|
||||
w.begin.p = (const uint8_t *)"0000";
|
||||
w.begin.len = 4;
|
||||
w.end.len = 0;
|
||||
w.writeVersion = 1;
|
||||
ConflictSet_addWrites(cs, &w, 1);
|
||||
ConflictSet_addWrites(cs, &w, 1, 1);
|
||||
r.begin.p = (const uint8_t *)"0000";
|
||||
r.begin.len = 4;
|
||||
r.end.len = 0;
|
||||
r.readVersion = 0;
|
||||
ConflictSet_check(cs, &r, &result, 1);
|
||||
assert(result == ConflictSet_Conflict);
|
||||
bytes = ConflictSet_getBytes(cs);
|
||||
assert(bytes > 0);
|
||||
ConflictSet_destroy(cs);
|
||||
return 0;
|
||||
}
|
||||
|
@@ -2,14 +2,15 @@
|
||||
|
||||
#include <cassert>
|
||||
|
||||
using namespace weaselab;
|
||||
|
||||
int main(void) {
|
||||
ConflictSet cs(0);
|
||||
ConflictSet::WriteRange w;
|
||||
w.begin.p = (const uint8_t *)"0000";
|
||||
w.begin.len = 4;
|
||||
w.end.len = 0;
|
||||
w.writeVersion = 1;
|
||||
cs.addWrites(&w, 1);
|
||||
cs.addWrites(&w, 1, 1);
|
||||
ConflictSet::Result result;
|
||||
ConflictSet::ReadRange r;
|
||||
r.begin.p = (const uint8_t *)"0000";
|
||||
@@ -18,4 +19,6 @@ int main(void) {
|
||||
r.readVersion = 0;
|
||||
cs.check(&r, &result, 1);
|
||||
assert(result == ConflictSet::Conflict);
|
||||
int64_t bytes = cs.getBytes();
|
||||
assert(bytes > 0);
|
||||
}
|
||||
|
BIN
corpus/00cc0f31d9e079e5c10967791cbfc214c876da48
Normal file
BIN
corpus/00cc0f31d9e079e5c10967791cbfc214c876da48
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
corpus/02057227d6245d09b874d53295f14ddb87ee54ff
Normal file
BIN
corpus/02057227d6245d09b874d53295f14ddb87ee54ff
Normal file
Binary file not shown.
BIN
corpus/0258a68ab599675ffb6639a7e1e3cea2879c79cf
Normal file
BIN
corpus/0258a68ab599675ffb6639a7e1e3cea2879c79cf
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
corpus/0325dc4513367ff3f788d540a3d5037cd8422645
Normal file
BIN
corpus/0325dc4513367ff3f788d540a3d5037cd8422645
Normal file
Binary file not shown.
BIN
corpus/0415011bcf7b79337a7fb013d85f9b840b8d2050
Normal file
BIN
corpus/0415011bcf7b79337a7fb013d85f9b840b8d2050
Normal file
Binary file not shown.
Binary file not shown.
BIN
corpus/0503541cc3e72b44e61518ff3d525f226c64ace7
Normal file
BIN
corpus/0503541cc3e72b44e61518ff3d525f226c64ace7
Normal file
Binary file not shown.
BIN
corpus/05bef24faf30d29da5b7caa52f80302985c00e8c
Normal file
BIN
corpus/05bef24faf30d29da5b7caa52f80302985c00e8c
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
corpus/063d680d490143cf5fbcb7414f97affed2f66493
Normal file
BIN
corpus/063d680d490143cf5fbcb7414f97affed2f66493
Normal file
Binary file not shown.
Binary file not shown.
BIN
corpus/064b20fcb90c53503dbb803dc85ff3c5427fe469
Normal file
BIN
corpus/064b20fcb90c53503dbb803dc85ff3c5427fe469
Normal file
Binary file not shown.
BIN
corpus/072efe2ef3d857c2a7d417e010933d496386ea2b
Normal file
BIN
corpus/072efe2ef3d857c2a7d417e010933d496386ea2b
Normal file
Binary file not shown.
Binary file not shown.
BIN
corpus/07caf39a29c8f5f703a85dabf1069073f1155f9a
Normal file
BIN
corpus/07caf39a29c8f5f703a85dabf1069073f1155f9a
Normal file
Binary file not shown.
BIN
corpus/080c4a75f5549a222909e354ac0d00c63f11701a
Normal file
BIN
corpus/080c4a75f5549a222909e354ac0d00c63f11701a
Normal file
Binary file not shown.
BIN
corpus/09577892321d14b660c77955dc05ba4cf9bcc05c
Normal file
BIN
corpus/09577892321d14b660c77955dc05ba4cf9bcc05c
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
corpus/0b2b81913b5ef8e7dc1c0d9486f5dd1df578069c
Normal file
BIN
corpus/0b2b81913b5ef8e7dc1c0d9486f5dd1df578069c
Normal file
Binary file not shown.
BIN
corpus/0cab3671b418e023a9b955a5bcdb339a5d80885d
Normal file
BIN
corpus/0cab3671b418e023a9b955a5bcdb339a5d80885d
Normal file
Binary file not shown.
BIN
corpus/0d668e5a8e3e3fc505d99bb1b0fb2633bc8e616c
Normal file
BIN
corpus/0d668e5a8e3e3fc505d99bb1b0fb2633bc8e616c
Normal file
Binary file not shown.
BIN
corpus/0dd7586cd48024a419c0ab49808b958d6540becc
Normal file
BIN
corpus/0dd7586cd48024a419c0ab49808b958d6540becc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
corpus/0e8030a76f0434cddf6b81ca053e3cc208c2ea03
Normal file
BIN
corpus/0e8030a76f0434cddf6b81ca053e3cc208c2ea03
Normal file
Binary file not shown.
BIN
corpus/0ebb5811e76d95913a595fdeb53d8d440c84d027
Normal file
BIN
corpus/0ebb5811e76d95913a595fdeb53d8d440c84d027
Normal file
Binary file not shown.
Binary file not shown.
BIN
corpus/0f5349817b85b03ef24015b1cda7aa69c06e53d9
Normal file
BIN
corpus/0f5349817b85b03ef24015b1cda7aa69c06e53d9
Normal file
Binary file not shown.
BIN
corpus/0f8b6a2d4ebe4472b25b81d4e8d35586c75af9f9
Normal file
BIN
corpus/0f8b6a2d4ebe4472b25b81d4e8d35586c75af9f9
Normal file
Binary file not shown.
Binary file not shown.
BIN
corpus/13f781904a9065579d637731c787998f305cc860
Normal file
BIN
corpus/13f781904a9065579d637731c787998f305cc860
Normal file
Binary file not shown.
Binary file not shown.
BIN
corpus/15d58ff012753e558bfc8e5ff95e456a24654fe1
Normal file
BIN
corpus/15d58ff012753e558bfc8e5ff95e456a24654fe1
Normal file
Binary file not shown.
BIN
corpus/15ff9acbb757117edbc8fcafa13a902f8dfad08d
Normal file
BIN
corpus/15ff9acbb757117edbc8fcafa13a902f8dfad08d
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
corpus/18a37da332a8fc0b894fb32da8aa17e981a20e16
Normal file
BIN
corpus/18a37da332a8fc0b894fb32da8aa17e981a20e16
Normal file
Binary file not shown.
BIN
corpus/18e0a1a32d1dfac596258d76367fe3d072863cab
Normal file
BIN
corpus/18e0a1a32d1dfac596258d76367fe3d072863cab
Normal file
Binary file not shown.
Binary file not shown.
BIN
corpus/199a623aac6456b63c51775bfc307f92af877ee9
Normal file
BIN
corpus/199a623aac6456b63c51775bfc307f92af877ee9
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
corpus/1b1ee095f24377da663057a4c055795fac40bfda
Normal file
BIN
corpus/1b1ee095f24377da663057a4c055795fac40bfda
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
corpus/1ca245aada69d5d286c8e8782f03b2b5503a26f5
Normal file
BIN
corpus/1ca245aada69d5d286c8e8782f03b2b5503a26f5
Normal file
Binary file not shown.
Binary file not shown.
BIN
corpus/1e24a4e9325031dadafecedb99f15b87e97a1812
Normal file
BIN
corpus/1e24a4e9325031dadafecedb99f15b87e97a1812
Normal file
Binary file not shown.
BIN
corpus/1e33bf9cb1d0e84d9c9780608b43444495c4d8bf
Normal file
BIN
corpus/1e33bf9cb1d0e84d9c9780608b43444495c4d8bf
Normal file
Binary file not shown.
Binary file not shown.
BIN
corpus/1fcf4bf1173d1054a5e85375ff7667f392508faa
Normal file
BIN
corpus/1fcf4bf1173d1054a5e85375ff7667f392508faa
Normal file
Binary file not shown.
Binary file not shown.
BIN
corpus/20a353ee3251c2f27b27794c4118cebb24d16b59
Normal file
BIN
corpus/20a353ee3251c2f27b27794c4118cebb24d16b59
Normal file
Binary file not shown.
BIN
corpus/20df7c1e5e3acf2e9d7b050a7dd591b4ace6819e
Normal file
BIN
corpus/20df7c1e5e3acf2e9d7b050a7dd591b4ace6819e
Normal file
Binary file not shown.
Binary file not shown.
BIN
corpus/218f8656038aeb0bfd406d7abf7908501243a181
Normal file
BIN
corpus/218f8656038aeb0bfd406d7abf7908501243a181
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
corpus/234e5cd85e9ffa7fa5a4ae6491025898ac412ac5
Normal file
BIN
corpus/234e5cd85e9ffa7fa5a4ae6491025898ac412ac5
Normal file
Binary file not shown.
BIN
corpus/23f00c1ae1c35400efbbeb2156f6e9d45f6bfcce
Normal file
BIN
corpus/23f00c1ae1c35400efbbeb2156f6e9d45f6bfcce
Normal file
Binary file not shown.
BIN
corpus/26915f2b14f80fb44d34ef855d6ffeff2926138a
Normal file
BIN
corpus/26915f2b14f80fb44d34ef855d6ffeff2926138a
Normal file
Binary file not shown.
BIN
corpus/26eac0247cc3a68355f59134db85969b76d4e701
Normal file
BIN
corpus/26eac0247cc3a68355f59134db85969b76d4e701
Normal file
Binary file not shown.
BIN
corpus/28f5bafcb2c22604c19e64ecf79ff926dcfb942a
Normal file
BIN
corpus/28f5bafcb2c22604c19e64ecf79ff926dcfb942a
Normal file
Binary file not shown.
BIN
corpus/291bd1320f380ad648f9bd8c2f3c80de3bd2d86d
Normal file
BIN
corpus/291bd1320f380ad648f9bd8c2f3c80de3bd2d86d
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
corpus/2a666bfbb2b22b1202d23af4b9b35e56eea5e951
Normal file
BIN
corpus/2a666bfbb2b22b1202d23af4b9b35e56eea5e951
Normal file
Binary file not shown.
Binary file not shown.
BIN
corpus/2a87229ba138b7bb2bf3beaf0367ceaca58b5076
Normal file
BIN
corpus/2a87229ba138b7bb2bf3beaf0367ceaca58b5076
Normal file
Binary file not shown.
BIN
corpus/2c2b15e9f8fa607cabb128bec2181d235264b617
Normal file
BIN
corpus/2c2b15e9f8fa607cabb128bec2181d235264b617
Normal file
Binary file not shown.
BIN
corpus/2d97d47f77bf81d8e02b14c54151ab2519b2d849
Normal file
BIN
corpus/2d97d47f77bf81d8e02b14c54151ab2519b2d849
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user