Compare commits
19
Commits
a792352387
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e23018c952 | ||
|
|
b1c1f8f2ee | ||
|
|
4bec2cf4eb | ||
|
|
fa1701479c | ||
|
|
20e2e332e4 | ||
|
|
23d092ab69 | ||
|
|
84bf5707f9 | ||
|
|
c94e9aaee6 | ||
|
|
d71d963d9a | ||
|
|
dabddbcb56 | ||
|
|
3b4271dac1 | ||
|
|
56e0673fc7 | ||
|
|
80f084cc43 | ||
|
|
c0bd10effa | ||
|
|
df986810ff | ||
|
|
8b6389236a | ||
|
|
b22d5f4816 | ||
|
|
bba328f79f | ||
|
|
3d3184ef5f |
No files matched your search
+23
-2
@@ -182,6 +182,8 @@ endif()
|
||||
|
||||
include(CTest)
|
||||
|
||||
option(TEST_SKIP_LIST "Run the slow skip list cross-check tests" OFF)
|
||||
|
||||
# disable tests if this is being used through e.g. FetchContent
|
||||
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR AND BUILD_TESTING)
|
||||
|
||||
@@ -224,8 +226,9 @@ if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR AND BUILD_TESTING)
|
||||
target_compile_options(driver_skip_list PRIVATE ${TEST_FLAGS})
|
||||
target_link_libraries(driver_skip_list PRIVATE skip_list)
|
||||
|
||||
# enable to test skip list
|
||||
if(0)
|
||||
# Optional cross-check of SkipList.cpp against ReferenceImpl. Opt-in via
|
||||
# -DTEST_SKIP_LIST=ON, since these are slow.
|
||||
if(TEST_SKIP_LIST)
|
||||
foreach(TEST ${CORPUS_TESTS})
|
||||
get_filename_component(hash ${TEST} NAME)
|
||||
add_test(NAME skip_list_${hash} COMMAND driver_skip_list ${TEST})
|
||||
@@ -390,6 +393,24 @@ if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR AND BUILD_TESTING)
|
||||
PROPERTIES CXX_STANDARD_REQUIRED ON)
|
||||
add_test(NAME conflict_set_cxx_api_test COMMAND conflict_set_cxx_api_test)
|
||||
|
||||
# SHOW_MEMORY instrumentation, which the rest of the test suite doesn't
|
||||
# compile with
|
||||
add_executable(conflict_set_show_memory_test ConflictSet.cpp
|
||||
ShowMemoryTest.cpp)
|
||||
target_compile_options(conflict_set_show_memory_test PRIVATE ${TEST_FLAGS})
|
||||
target_compile_definitions(conflict_set_show_memory_test
|
||||
PRIVATE SHOW_MEMORY=1)
|
||||
target_include_directories(conflict_set_show_memory_test
|
||||
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
|
||||
if(NOT CMAKE_CROSSCOMPILING)
|
||||
target_compile_options(conflict_set_show_memory_test
|
||||
PRIVATE -fsanitize=address,undefined)
|
||||
target_link_options(conflict_set_show_memory_test PRIVATE
|
||||
-fsanitize=address,undefined)
|
||||
endif()
|
||||
add_test(NAME conflict_set_show_memory_test
|
||||
COMMAND conflict_set_show_memory_test)
|
||||
|
||||
# symbol visibility tests
|
||||
if(NOT CMAKE_BUILD_TYPE STREQUAL Debug)
|
||||
if(APPLE)
|
||||
|
||||
+151
-21
@@ -80,17 +80,19 @@ using namespace weaselab;
|
||||
#define assume assert
|
||||
#endif
|
||||
|
||||
#if SHOW_MEMORY
|
||||
// The SHOW_MEMORY instrumentation. Declared unconditionally so that each of
|
||||
// these has a single definition in every configuration (see the definitions at
|
||||
// the end of this file), rather than a no-op stub standing in for the real
|
||||
// thing.
|
||||
void addNode(struct Node *);
|
||||
void removeNode(struct Node *);
|
||||
void addKey(struct Node *);
|
||||
void removeKey(struct Node *);
|
||||
#else
|
||||
constexpr void addNode(struct Node *) {}
|
||||
constexpr void removeNode(struct Node *) {}
|
||||
constexpr void addKey(struct Node *) {}
|
||||
constexpr void removeKey(struct Node *) {}
|
||||
#endif
|
||||
// Like `removeKey`, but with a caller-provided search path length. Used when
|
||||
// the node's ancestors may already have been released, which makes walking
|
||||
// `parent` pointers invalid.
|
||||
void removeKey(struct Node *, int64_t searchPathLength);
|
||||
int64_t getSearchPathLength(struct Node *);
|
||||
|
||||
// ==================== BEGIN IMPLEMENTATION ====================
|
||||
|
||||
@@ -116,13 +118,40 @@ struct InternalVersionT {
|
||||
constexpr InternalVersionT() = default;
|
||||
constexpr explicit InternalVersionT(int64_t value) : value(value) {}
|
||||
constexpr int64_t toInt64() const { return value; } // GCOVR_EXCL_LINE
|
||||
constexpr auto operator<=>(const InternalVersionT &rhs) const {
|
||||
constexpr bool operator<(const InternalVersionT &rhs) const {
|
||||
#if USE_64_BIT
|
||||
return value <=> rhs.value;
|
||||
return value < rhs.value;
|
||||
#else
|
||||
// Maintains ordering after overflow, as long as the full-precision versions
|
||||
// are within `kMaxCorrectVersionWindow` of eachother.
|
||||
return int32_t(value - rhs.value) <=> 0;
|
||||
return int32_t(value - rhs.value) < 0;
|
||||
#endif
|
||||
}
|
||||
constexpr bool operator<=(const InternalVersionT &rhs) const {
|
||||
#if USE_64_BIT
|
||||
return value <= rhs.value;
|
||||
#else
|
||||
// Maintains ordering after overflow, as long as the full-precision versions
|
||||
// are within `kMaxCorrectVersionWindow` of eachother.
|
||||
return int32_t(value - rhs.value) <= 0;
|
||||
#endif
|
||||
}
|
||||
constexpr bool operator>=(const InternalVersionT &rhs) const {
|
||||
#if USE_64_BIT
|
||||
return value >= rhs.value;
|
||||
#else
|
||||
// Maintains ordering after overflow, as long as the full-precision versions
|
||||
// are within `kMaxCorrectVersionWindow` of eachother.
|
||||
return int32_t(value - rhs.value) >= 0;
|
||||
#endif
|
||||
}
|
||||
constexpr bool operator>(const InternalVersionT &rhs) const {
|
||||
#if USE_64_BIT
|
||||
return value > rhs.value;
|
||||
#else
|
||||
// Maintains ordering after overflow, as long as the full-precision versions
|
||||
// are within `kMaxCorrectVersionWindow` of eachother.
|
||||
return int32_t(value - rhs.value) > 0;
|
||||
#endif
|
||||
}
|
||||
constexpr bool operator==(const InternalVersionT &) const = default;
|
||||
@@ -307,7 +336,9 @@ struct Node {
|
||||
uint8_t *partialKey();
|
||||
Type getType() const { return type; }
|
||||
int32_t getCapacity() const {
|
||||
assert(!releaseDeferred);
|
||||
// `partialKeyCapacity` is not repurposed by `deferRelease`, so this is also
|
||||
// valid for nodes with `releaseDeferred` set. It's needed to account for
|
||||
// the capacity of a deferred node when it's finally released.
|
||||
return partialKeyCapacity;
|
||||
}
|
||||
|
||||
@@ -2280,6 +2311,8 @@ bool checkMaxBetweenExclusiveImpl(Node3 *n, int begin, int end,
|
||||
assert(!(begin == -1 && end == 256));
|
||||
|
||||
auto *self = static_cast<Node3 *>(n);
|
||||
assume(1 <= self->numChildren);
|
||||
assume(self->numChildren <= 3);
|
||||
|
||||
++begin;
|
||||
|
||||
@@ -2753,10 +2786,39 @@ checkMaxBetweenExclusiveImpl<true>(Node256 *n, int begin, int end,
|
||||
return self;
|
||||
}
|
||||
|
||||
#if SHOW_MEMORY
|
||||
// Length of the key leading to `child`, given the length of the key leading to
|
||||
// its parent. Consistent with `getSearchPathLength`.
|
||||
int64_t childSearchPathLength(int64_t parentSearchPathLength, Node *child) {
|
||||
return parentSearchPathLength + 1 + child->partialKeyLen;
|
||||
}
|
||||
#endif
|
||||
|
||||
void eraseTree(Node *root, WriteContext *writeContext) {
|
||||
Arena arena;
|
||||
auto toFree = vector<Node *>(arena);
|
||||
toFree.push_back(root);
|
||||
// Length of the key leading to each node in `toFree`, on a parallel stack.
|
||||
// A node is released before its children are visited, so the `parent`
|
||||
// pointers that `getSearchPathLength` walks are dangling by the time a queued
|
||||
// node is popped. Carry the lengths along instead. Only the SHOW_MEMORY
|
||||
// accounting needs them.
|
||||
#if SHOW_MEMORY
|
||||
auto searchPathLengths = vector<int64_t>(arena);
|
||||
searchPathLengths.push_back(getSearchPathLength(root));
|
||||
#endif
|
||||
|
||||
// Queue `child` for release, where `parentSearchPathLength` is the length of
|
||||
// the key leading to its parent.
|
||||
auto queueChild = [&](Node *child, int64_t parentSearchPathLength) {
|
||||
toFree.push_back(child);
|
||||
#if SHOW_MEMORY
|
||||
searchPathLengths.push_back(
|
||||
childSearchPathLength(parentSearchPathLength, child));
|
||||
#else
|
||||
(void)parentSearchPathLength;
|
||||
#endif
|
||||
};
|
||||
|
||||
while (toFree.size() > 0) {
|
||||
auto *n = toFree.back();
|
||||
@@ -2764,7 +2826,13 @@ void eraseTree(Node *root, WriteContext *writeContext) {
|
||||
writeContext->accum.entries_erased += n->entryPresent;
|
||||
++writeContext->accum.nodes_released;
|
||||
|
||||
removeKey(n);
|
||||
#if SHOW_MEMORY
|
||||
const int64_t searchPathLength = searchPathLengths.back();
|
||||
searchPathLengths.pop_back();
|
||||
#else
|
||||
const int64_t searchPathLength = 0;
|
||||
#endif
|
||||
removeKey(n, searchPathLength);
|
||||
|
||||
switch (n->getType()) {
|
||||
case Type_Node0: {
|
||||
@@ -2774,28 +2842,39 @@ void eraseTree(Node *root, WriteContext *writeContext) {
|
||||
case Type_Node3: {
|
||||
auto *n3 = static_cast<Node3 *>(n);
|
||||
for (int i = 0; i < n3->numChildren; ++i) {
|
||||
toFree.push_back(n3->children[i]);
|
||||
queueChild(n3->children[i], searchPathLength);
|
||||
}
|
||||
writeContext->release(n3);
|
||||
} break;
|
||||
case Type_Node16: {
|
||||
auto *n16 = static_cast<Node16 *>(n);
|
||||
for (int i = 0; i < n16->numChildren; ++i) {
|
||||
toFree.push_back(n16->children[i]);
|
||||
queueChild(n16->children[i], searchPathLength);
|
||||
}
|
||||
writeContext->release(n16);
|
||||
} break;
|
||||
case Type_Node48: {
|
||||
auto *n48 = static_cast<Node48 *>(n);
|
||||
for (int i = 0; i < n48->numChildren; ++i) {
|
||||
toFree.push_back(n48->children[i]);
|
||||
queueChild(n48->children[i], searchPathLength);
|
||||
}
|
||||
writeContext->release(n48);
|
||||
} break;
|
||||
case Type_Node256: {
|
||||
auto *n256 = static_cast<Node256 *>(n);
|
||||
auto *out = toFree.unsafePrepareAppend(n256->numChildren).data();
|
||||
n256->bitSet.forEachSet([&](int i) { *out++ = n256->children[i]; });
|
||||
#if SHOW_MEMORY
|
||||
auto *outSearchPathLengths =
|
||||
searchPathLengths.unsafePrepareAppend(n256->numChildren).data();
|
||||
#endif
|
||||
n256->bitSet.forEachSet([&](int i) {
|
||||
Node *child = n256->children[i];
|
||||
*out++ = child;
|
||||
#if SHOW_MEMORY
|
||||
*outSearchPathLengths++ =
|
||||
childSearchPathLength(searchPathLength, child);
|
||||
#endif
|
||||
});
|
||||
assert(out == toFree.end());
|
||||
writeContext->release(n256);
|
||||
} break;
|
||||
@@ -2907,9 +2986,13 @@ void fixupMaxVersion(Node *node, WriteContext *writeContext) {
|
||||
break;
|
||||
case Type_Node3: {
|
||||
auto *self3 = static_cast<Node3 *>(node);
|
||||
max = std::max(max,
|
||||
horizontalMaxUpTo16(self3->childMaxVersion,
|
||||
writeContext->zero, self3->numChildren));
|
||||
assume(1 <= self3->numChildren);
|
||||
assume(self3->numChildren <= 3);
|
||||
InternalVersionT m = self3->childMaxVersion[0];
|
||||
for (int i = 1; i < self3->numChildren; ++i) {
|
||||
m = std::max(m, self3->childMaxVersion[i]);
|
||||
}
|
||||
max = std::max(max, m);
|
||||
} break;
|
||||
case Type_Node16: {
|
||||
auto *self16 = static_cast<Node16 *>(node);
|
||||
@@ -5463,8 +5546,9 @@ void internal_addWrites(ConflictSet::Impl *impl,
|
||||
impl->totalBytes += mallocBytesDelta;
|
||||
impl->memory_bytes.set(impl->totalBytes);
|
||||
#if SHOW_MEMORY
|
||||
// Testing-only cross-check of the accounting.
|
||||
if (impl->totalBytes != mallocBytes) {
|
||||
abort();
|
||||
abort(); // GCOVR_EXCL_LINE
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -5475,8 +5559,9 @@ void internal_setOldestVersion(ConflictSet::Impl *impl, int64_t oldestVersion) {
|
||||
impl->totalBytes += mallocBytesDelta;
|
||||
impl->memory_bytes.set(impl->totalBytes);
|
||||
#if SHOW_MEMORY
|
||||
// Testing-only cross-check of the accounting.
|
||||
if (impl->totalBytes != mallocBytes) {
|
||||
abort();
|
||||
abort(); // GCOVR_EXCL_LINE
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -5942,6 +6027,14 @@ checkMaxVersion(Node *root, Node *node, InternalVersionT oldestVersion,
|
||||
|
||||
} // namespace
|
||||
|
||||
// The SHOW_MEMORY instrumentation. Each of these functions is defined exactly
|
||||
// once, with the accounting compiled out when SHOW_MEMORY is disabled. Defining
|
||||
// them once (rather than defining no-op stubs when SHOW_MEMORY is disabled)
|
||||
// keeps every configuration of this file reporting each function on the same
|
||||
// line, which merging coverage from objects compiled with different options
|
||||
// requires: gcovr, merging the SHOW_MEMORY test with the rest of the build,
|
||||
// otherwise aborts with "Got function ... on multiple lines". The optimizer
|
||||
// removes the resulting empty calls.
|
||||
#if SHOW_MEMORY
|
||||
|
||||
int64_t nodeBytes = 0;
|
||||
@@ -5970,7 +6063,10 @@ int64_t getNodeSize(struct Node *n) {
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
int64_t getSearchPathLength(Node *n) {
|
||||
#if SHOW_MEMORY
|
||||
assert(n != nullptr);
|
||||
int64_t result = 0;
|
||||
for (;;) {
|
||||
@@ -5982,9 +6078,14 @@ int64_t getSearchPathLength(Node *n) {
|
||||
n = n->parent;
|
||||
}
|
||||
return result;
|
||||
#else
|
||||
(void)n;
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
void addNode(Node *n) {
|
||||
#if SHOW_MEMORY
|
||||
nodeBytes += getNodeSize(n);
|
||||
partialCapacityBytes += n->getCapacity();
|
||||
if (nodeBytes > peakNodeBytes) {
|
||||
@@ -5993,14 +6094,22 @@ void addNode(Node *n) {
|
||||
if (partialCapacityBytes > peakPartialCapacityBytes) {
|
||||
peakPartialCapacityBytes = partialCapacityBytes;
|
||||
}
|
||||
#else
|
||||
(void)n;
|
||||
#endif
|
||||
}
|
||||
|
||||
void removeNode(Node *n) {
|
||||
#if SHOW_MEMORY
|
||||
nodeBytes -= getNodeSize(n);
|
||||
partialCapacityBytes -= n->getCapacity();
|
||||
#else
|
||||
(void)n;
|
||||
#endif
|
||||
}
|
||||
|
||||
void addKey(Node *n) {
|
||||
#if SHOW_MEMORY
|
||||
if (!n->entryPresent) {
|
||||
++totalKeys;
|
||||
keyBytes += getSearchPathLength(n);
|
||||
@@ -6011,15 +6120,36 @@ void addKey(Node *n) {
|
||||
peakKeyBytes = keyBytes;
|
||||
}
|
||||
}
|
||||
#else
|
||||
(void)n;
|
||||
#endif
|
||||
}
|
||||
|
||||
void removeKey(Node *n) {
|
||||
#if SHOW_MEMORY
|
||||
if (n->entryPresent) {
|
||||
--totalKeys;
|
||||
keyBytes -= getSearchPathLength(n);
|
||||
}
|
||||
#else
|
||||
(void)n;
|
||||
#endif
|
||||
}
|
||||
|
||||
void removeKey(Node *n, int64_t searchPathLength) {
|
||||
#if SHOW_MEMORY
|
||||
if (n->entryPresent) {
|
||||
--totalKeys;
|
||||
keyBytes -= searchPathLength;
|
||||
}
|
||||
#else
|
||||
(void)n;
|
||||
(void)searchPathLength;
|
||||
#endif
|
||||
}
|
||||
|
||||
#if SHOW_MEMORY
|
||||
|
||||
struct __attribute__((visibility("default"))) PeakPrinter {
|
||||
~PeakPrinter() {
|
||||
printf("--- radix_tree ---\n");
|
||||
|
||||
@@ -23,7 +23,9 @@ using namespace weaselab;
|
||||
#include <callgrind.h>
|
||||
|
||||
#define DEBUG_VERBOSE 0
|
||||
#ifndef SHOW_MEMORY
|
||||
#define SHOW_MEMORY 0
|
||||
#endif
|
||||
|
||||
// std::span is not trivially constructible. We want a span that leaves its
|
||||
// members uninitialized for performance reasons.
|
||||
@@ -96,6 +98,11 @@ static_assert(std::is_trivial_v<TrivialSpan>);
|
||||
#if SHOW_MEMORY
|
||||
inline int64_t mallocBytes = 0;
|
||||
inline int64_t peakMallocBytes = 0;
|
||||
// Defined in ConflictSet.cpp
|
||||
extern int64_t nodeBytes;
|
||||
extern int64_t partialCapacityBytes;
|
||||
extern int64_t totalKeys;
|
||||
extern int64_t keyBytes;
|
||||
#endif
|
||||
|
||||
inline thread_local int64_t mallocBytesDelta = 0;
|
||||
|
||||
+71
-3
@@ -96,7 +96,7 @@ inline int firstNeqStride(const uint8_t *ap, const uint8_t *bp) {
|
||||
// GCOVR_EXCL_START
|
||||
#if defined(HAS_AVX) && !defined(__SANITIZE_THREAD__)
|
||||
__attribute__((target("avx512f,avx512bw"))) inline int
|
||||
longestCommonPrefix(const uint8_t *ap, const uint8_t *bp, int cl) {
|
||||
longestCommonPrefixLong(const uint8_t *ap, const uint8_t *bp, int cl) {
|
||||
int i = 0;
|
||||
int end = cl & ~63;
|
||||
while (i < end) {
|
||||
@@ -123,7 +123,7 @@ __attribute__((target("default")))
|
||||
// GCOVR_EXCL_STOP
|
||||
|
||||
inline int
|
||||
longestCommonPrefix(const uint8_t *ap, const uint8_t *bp, int cl) {
|
||||
longestCommonPrefixLong(const uint8_t *ap, const uint8_t *bp, int cl) {
|
||||
if (!(cl >= 0)) {
|
||||
__builtin_unreachable(); // GCOVR_EXCL_LINE
|
||||
}
|
||||
@@ -182,4 +182,72 @@ longestCommonPrefix(const uint8_t *ap, const uint8_t *bp, int cl) {
|
||||
}
|
||||
|
||||
return i;
|
||||
}
|
||||
}
|
||||
// Use in-bounds native-word loads for short common lengths. The second load
|
||||
// overlaps the first; because the first load matched, any mismatch found in
|
||||
// the overlap is impossible and the returned absolute index is still correct.
|
||||
// Precondition: cl <= 2 * sizeof(uint64_t)
|
||||
__attribute__((always_inline)) static inline int
|
||||
longestCommonPrefixShort(const uint8_t *ap, const uint8_t *bp, int cl) {
|
||||
assert(cl <= 2 * int(sizeof(uint64_t)));
|
||||
if (cl >= 8) {
|
||||
uint64_t a0;
|
||||
uint64_t b0;
|
||||
memcpy(&a0, ap, sizeof(a0));
|
||||
memcpy(&b0, bp, sizeof(b0));
|
||||
if (uint64_t mismatched = a0 ^ b0) {
|
||||
return std::countr_zero(mismatched) / 8;
|
||||
}
|
||||
if (cl == 8) {
|
||||
return 8;
|
||||
}
|
||||
|
||||
uint64_t a1;
|
||||
uint64_t b1;
|
||||
memcpy(&a1, ap + cl - 8, sizeof(a1));
|
||||
memcpy(&b1, bp + cl - 8, sizeof(b1));
|
||||
if (uint64_t mismatched = a1 ^ b1) {
|
||||
return cl - 8 + std::countr_zero(mismatched) / 8;
|
||||
}
|
||||
return cl;
|
||||
}
|
||||
|
||||
if (cl >= 4) {
|
||||
uint32_t a0;
|
||||
uint32_t b0;
|
||||
memcpy(&a0, ap, sizeof(a0));
|
||||
memcpy(&b0, bp, sizeof(b0));
|
||||
if (uint32_t mismatched = a0 ^ b0) {
|
||||
return std::countr_zero(mismatched) / 8;
|
||||
}
|
||||
if (cl == 4) {
|
||||
return 4;
|
||||
}
|
||||
|
||||
uint32_t a1;
|
||||
uint32_t b1;
|
||||
memcpy(&a1, ap + cl - 4, sizeof(a1));
|
||||
memcpy(&b1, bp + cl - 4, sizeof(b1));
|
||||
if (uint32_t mismatched = a1 ^ b1) {
|
||||
return cl - 4 + std::countr_zero(mismatched) / 8;
|
||||
}
|
||||
return cl;
|
||||
}
|
||||
|
||||
int i = 0;
|
||||
while (i < cl && ap[i] == bp[i]) {
|
||||
++i;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
__attribute__((always_inline)) static inline int
|
||||
longestCommonPrefix(const uint8_t *ap, const uint8_t *bp, int cl) {
|
||||
if (!(cl >= 0)) {
|
||||
__builtin_unreachable(); // GCOVR_EXCL_LINE
|
||||
}
|
||||
if (cl <= 2 * int(sizeof(uint64_t))) {
|
||||
return longestCommonPrefixShort(ap, bp, cl);
|
||||
}
|
||||
return longestCommonPrefixLong(ap, bp, cl);
|
||||
}
|
||||
@@ -19,30 +19,30 @@ InstalledDir: /usr/lib/llvm-21/bin
|
||||
|
||||
| ns/op | op/s | err% | ins/op | cyc/op | IPC | bra/op | miss% | total | benchmark
|
||||
|--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:----------
|
||||
| 164.29 | 6,086,873.38 | 0.0% | 3,107.03 | 604.19 | 5.142 | 558.59 | 0.0% | 1.96 | `point reads`
|
||||
| 161.05 | 6,209,395.38 | 0.1% | 3,036.76 | 592.21 | 5.128 | 539.35 | 0.0% | 1.93 | `prefix reads`
|
||||
| 239.55 | 4,174,539.38 | 0.1% | 3,722.71 | 880.68 | 4.227 | 692.00 | 0.0% | 2.86 | `range reads`
|
||||
| 354.75 | 2,818,919.14 | 0.7% | 4,523.64 | 1,304.75 | 3.467 | 720.22 | 2.0% | 4.23 | `point writes`
|
||||
| 345.32 | 2,895,878.47 | 0.1% | 4,484.57 | 1,270.31 | 3.530 | 705.00 | 1.8% | 4.12 | `prefix writes`
|
||||
| 193.48 | 5,168,547.42 | 0.1% | 2,224.10 | 711.72 | 3.125 | 377.17 | 3.3% | 2.32 | `range writes`
|
||||
| 404.89 | 2,469,777.50 | 2.4% | 6,855.96 | 1,489.70 | 4.602 | 1,227.82 | 1.3% | 0.05 | `monotonic increasing point writes`
|
||||
| 134,231.80 | 7,449.80 | 1.9% | 812,045.25 | 495,770.40 | 1.638 | 151,246.50 | 0.9% | 0.01 | `worst case for radix tree`
|
||||
| 37.80 | 26,454,311.17 | 0.4% | 701.00 | 139.14 | 5.038 | 102.00 | 0.0% | 0.01 | `create and destroy`
|
||||
| 165.93 | 6,026,520.04 | 0.1% | 3,107.03 | 610.42 | 5.090 | 558.59 | 0.0% | 1.98 | `point reads`
|
||||
| 163.08 | 6,132,046.57 | 0.1% | 3,036.76 | 599.68 | 5.064 | 539.35 | 0.0% | 1.95 | `prefix reads`
|
||||
| 250.26 | 3,995,834.21 | 0.1% | 3,722.71 | 920.25 | 4.045 | 692.00 | 0.0% | 2.99 | `range reads`
|
||||
| 359.64 | 2,780,581.63 | 0.1% | 4,523.64 | 1,322.63 | 3.420 | 720.22 | 2.1% | 4.30 | `point writes`
|
||||
| 358.20 | 2,791,726.93 | 0.1% | 4,484.57 | 1,317.23 | 3.405 | 705.00 | 2.0% | 4.28 | `prefix writes`
|
||||
| 193.91 | 5,157,081.29 | 0.1% | 2,224.10 | 713.33 | 3.118 | 377.17 | 3.3% | 2.32 | `range writes`
|
||||
| 422.03 | 2,369,513.38 | 1.1% | 6,855.96 | 1,551.55 | 4.419 | 1,227.82 | 1.4% | 0.05 | `monotonic increasing point writes`
|
||||
| 142,467.00 | 7,019.17 | 3.8% | 812,047.33 | 508,535.40 | 1.597 | 151,246.50 | 1.0% | 0.01 | `worst case for radix tree`
|
||||
| 37.66 | 26,554,217.74 | 0.8% | 701.00 | 138.80 | 5.050 | 102.00 | 0.0% | 0.01 | `create and destroy`
|
||||
|
||||
## Radix tree (this implementation)
|
||||
|
||||
|
||||
| ns/op | op/s | err% | ins/op | cyc/op | IPC | bra/op | miss% | total | benchmark
|
||||
|--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:----------
|
||||
| 12.89 | 77,565,115.56 | 0.1% | 244.55 | 47.43 | 5.155 | 34.21 | 0.6% | 0.15 | `point reads`
|
||||
| 15.11 | 66,162,047.76 | 0.1% | 297.79 | 55.60 | 5.356 | 43.23 | 0.4% | 0.18 | `prefix reads`
|
||||
| 36.29 | 27,559,358.29 | 0.1% | 783.16 | 133.44 | 5.869 | 109.52 | 0.2% | 0.43 | `range reads`
|
||||
| 20.53 | 48,719,405.55 | 0.1% | 381.81 | 75.51 | 5.057 | 51.04 | 0.5% | 0.25 | `point writes`
|
||||
| 39.37 | 25,402,042.40 | 0.1% | 685.00 | 144.83 | 4.730 | 106.72 | 0.3% | 0.47 | `prefix writes`
|
||||
| 43.78 | 22,843,841.63 | 0.1% | 800.40 | 161.06 | 4.970 | 127.36 | 0.1% | 0.53 | `range writes`
|
||||
| 78.37 | 12,760,008.75 | 1.0% | 1,452.61 | 288.24 | 5.040 | 278.69 | 0.1% | 0.01 | `monotonic increasing point writes`
|
||||
| 322,885.50 | 3,097.07 | 1.5% | 4,362,382.00 | 1,183,852.00 | 3.685 | 765,301.00 | 0.1% | 0.01 | `worst case for radix tree`
|
||||
| 99.99 | 10,000,718.79 | 0.4% | 1,775.00 | 367.93 | 4.824 | 288.00 | 0.0% | 0.01 | `create and destroy`
|
||||
| 12.25 | 81,664,669.29 | 0.4% | 233.49 | 44.94 | 5.196 | 31.23 | 0.7% | 0.15 | `point reads`
|
||||
| 13.35 | 74,903,950.49 | 0.1% | 271.75 | 49.10 | 5.534 | 40.21 | 0.4% | 0.16 | `prefix reads`
|
||||
| 31.37 | 31,874,583.62 | 0.1% | 700.39 | 115.43 | 6.067 | 100.83 | 0.2% | 0.37 | `range reads`
|
||||
| 19.55 | 51,153,057.80 | 0.1% | 366.51 | 71.90 | 5.098 | 47.92 | 0.5% | 0.23 | `point writes`
|
||||
| 36.92 | 27,088,344.17 | 0.1% | 645.11 | 135.72 | 4.753 | 100.31 | 0.4% | 0.44 | `prefix writes`
|
||||
| 36.84 | 27,143,553.08 | 0.1% | 691.04 | 135.42 | 5.103 | 114.01 | 0.1% | 0.45 | `range writes`
|
||||
| 74.62 | 13,400,467.72 | 0.9% | 1,418.37 | 274.20 | 5.173 | 273.39 | 0.1% | 0.01 | `monotonic increasing point writes`
|
||||
| 352,671.00 | 2,835.50 | 15.3% | 3,687,535.00 | 1,287,119.00 | 2.865 | 712,030.00 | 0.1% | 0.01 | :wavy_dash: `worst case for radix tree` (Unstable with ~1.0 iters. Increase `minEpochIterations` to e.g. 10)
|
||||
| 96.63 | 10,348,729.42 | 1.2% | 1,766.00 | 355.53 | 4.967 | 283.00 | 0.0% | 0.01 | `create and destroy`
|
||||
|
||||
# "Real data" test
|
||||
|
||||
@@ -51,13 +51,13 @@ Point queries only. Gc ratio is the ratio of time spent doing garbage collection
|
||||
## skip list
|
||||
|
||||
```
|
||||
Check: 4.62967 seconds, 352.195 MB/s, Add: 3.34177 seconds, 167.771 MB/s, Gc ratio: 37.9399%, Peak idle memory: 5.51852e+06
|
||||
Check: 4.69875 seconds, 347.017 MB/s, Add: 3.39475 seconds, 165.153 MB/s, Gc ratio: 38.7414%, Peak idle memory: 5.51852e+06
|
||||
```
|
||||
|
||||
## radix tree
|
||||
|
||||
```
|
||||
Check: 1.00477 seconds, 1622.8 MB/s, Add: 1.21142 seconds, 462.808 MB/s, Gc ratio: 39.4716%, Peak idle memory: 2.0226e+06
|
||||
Check: 0.976745 seconds, 1669.37 MB/s, Add: 1.20464 seconds, 465.411 MB/s, Gc ratio: 39.4158%, Peak idle memory: 2.0226e+06
|
||||
```
|
||||
|
||||
## hash table
|
||||
@@ -65,6 +65,6 @@ Check: 1.00477 seconds, 1622.8 MB/s, Add: 1.21142 seconds, 462.808 MB/s, Gc rati
|
||||
(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: 0.854254 seconds, 1908.74 MB/s, Add: 0.632626 seconds, 886.232 MB/s, Gc ratio: 41.0827%, Peak idle memory: 0
|
||||
Check: 0.864589 seconds, 1885.92 MB/s, Add: 0.634604 seconds, 883.47 MB/s, Gc ratio: 41.184%, Peak idle memory: 0
|
||||
```
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
// Exercises the SHOW_MEMORY instrumentation, which the other tests don't
|
||||
// compile with (this target passes -DSHOW_MEMORY=1). The instrumentation has
|
||||
// its own consistency checks, which abort on failure, and asserts are enabled
|
||||
// by TEST_FLAGS. Only one ConflictSet may exist at a time, since the
|
||||
// accounting is process-wide.
|
||||
#include "ConflictSet.h"
|
||||
#include "Internal.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
using namespace weaselab;
|
||||
|
||||
namespace {
|
||||
|
||||
void writePoint(ConflictSet &cs, const std::string &key, int64_t writeVersion) {
|
||||
ConflictSet::WriteRange w{{(const uint8_t *)key.data(), int(key.size())},
|
||||
{nullptr, 0}};
|
||||
cs.addWrites(&w, 1, writeVersion);
|
||||
}
|
||||
|
||||
ConflictSet::Result checkPoint(const ConflictSet &cs, const std::string &key,
|
||||
int64_t readVersion) {
|
||||
ConflictSet::ReadRange r{{(const uint8_t *)key.data(), int(key.size())},
|
||||
{nullptr, 0},
|
||||
readVersion};
|
||||
ConflictSet::Result result;
|
||||
cs.check(&r, &result, 1);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Everything a ConflictSet allocated must have been accounted for and released
|
||||
// by the time it's destroyed.
|
||||
void checkFullyReleased() {
|
||||
assert(nodeBytes == 0);
|
||||
assert(partialCapacityBytes == 0);
|
||||
assert(totalKeys == 0);
|
||||
assert(keyBytes == 0);
|
||||
assert(mallocBytes == 0);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
// Splitting a node replaces it, which defers the release of the old node.
|
||||
// Releasing a deferred node must account for its capacity, and the key it
|
||||
// held must be accounted for without walking `parent` pointers of nodes that
|
||||
// have already been released.
|
||||
{
|
||||
ConflictSet cs{0};
|
||||
writePoint(cs, "a", 1);
|
||||
writePoint(cs, "ab", 2);
|
||||
assert(checkPoint(cs, "a", 0) == ConflictSet::Conflict);
|
||||
assert(checkPoint(cs, "ab", 0) == ConflictSet::Conflict);
|
||||
assert(checkPoint(cs, "b", 0) == ConflictSet::Commit);
|
||||
assert(nodeBytes > 0);
|
||||
assert(keyBytes > 0);
|
||||
}
|
||||
checkFullyReleased();
|
||||
|
||||
// Exceeding the version window erases the tree mid-workload.
|
||||
{
|
||||
ConflictSet cs{0};
|
||||
writePoint(cs, "abc", 1);
|
||||
const int64_t version = int64_t(1) << 40;
|
||||
writePoint(cs, "abc", version);
|
||||
assert(checkPoint(cs, "abc", 0) == ConflictSet::TooOld);
|
||||
assert(checkPoint(cs, "abc", version) == ConflictSet::Commit);
|
||||
}
|
||||
checkFullyReleased();
|
||||
|
||||
// Erase a tree with many nodes, both mid-workload and at destruction.
|
||||
{
|
||||
ConflictSet cs{0};
|
||||
for (int i = 0; i < 1000; ++i) {
|
||||
writePoint(cs, "prefix/" + std::to_string(i), i + 1);
|
||||
}
|
||||
const int64_t version = int64_t(1) << 40;
|
||||
writePoint(cs, "prefix/1", version);
|
||||
assert(checkPoint(cs, "prefix/1", version) == ConflictSet::Commit);
|
||||
|
||||
for (int i = 0; i < 1000; ++i) {
|
||||
writePoint(cs, "other/" + std::to_string(i), version + i + 1);
|
||||
}
|
||||
assert(checkPoint(cs, "other/999", version) == ConflictSet::Conflict);
|
||||
}
|
||||
checkFullyReleased();
|
||||
|
||||
// Grow a node through every node type, so that the accounting for each node
|
||||
// type's size is exercised, then erase the entries one at a time so that
|
||||
// nodes are released individually rather than as part of a whole tree.
|
||||
{
|
||||
ConflictSet cs{0};
|
||||
std::string key(1, '\0');
|
||||
for (int i = 0; i < 256; ++i) {
|
||||
key[0] = char(i);
|
||||
writePoint(cs, key, i + 1);
|
||||
}
|
||||
for (int i = 0; i < 256; ++i) {
|
||||
key[0] = char(i);
|
||||
assert(checkPoint(cs, key, i) == ConflictSet::Conflict);
|
||||
}
|
||||
|
||||
// Garbage collection erases entries that can only fail with TooOld anyway.
|
||||
// Doing it one version at a time walks the tree, erasing, downsizing and
|
||||
// releasing nodes as it goes.
|
||||
for (int i = 0; i < 256; ++i) {
|
||||
cs.setOldestVersion(i + 1);
|
||||
}
|
||||
key[0] = '\0';
|
||||
assert(checkPoint(cs, key, 0) == ConflictSet::TooOld);
|
||||
}
|
||||
checkFullyReleased();
|
||||
}
|
||||
+8
-1
@@ -819,10 +819,17 @@ struct __attribute__((visibility("hidden"))) ConflictSet::Impl {
|
||||
}
|
||||
|
||||
void setOldestVersion(int64_t oldestVersion) {
|
||||
// oldestVersion becomes the max of its previous value and the argument, so
|
||||
// an older version is a no-op. We still fall through in the equal case so
|
||||
// that garbage collection can make progress without advancing the oldest
|
||||
// version.
|
||||
if (oldestVersion < this->oldestVersion) {
|
||||
return;
|
||||
}
|
||||
|
||||
// This isn't 100% accurate. It overcounts if you hit the end
|
||||
gc_iterations_total.add(keyUpdates);
|
||||
|
||||
assert(oldestVersion >= this->oldestVersion);
|
||||
this->oldestVersion = oldestVersion;
|
||||
SkipList::Finger finger;
|
||||
int temp;
|
||||
|
||||
+39
-5
@@ -68,6 +68,35 @@ def test_hash_table_getBytes():
|
||||
assert cs.check(read(0, b"key")) == [Result.CONFLICT]
|
||||
|
||||
|
||||
def test_setOldestVersion_does_not_decrease():
|
||||
# Regression test for issue #87: `setOldestVersion` sets oldestVersion to
|
||||
# the maximum of its previous value and its argument, so passing an older
|
||||
# version is valid input and must be a no-op. skip_list used to assert in
|
||||
# Debug builds and, in Release builds, lower its oldest version, which
|
||||
# resurrected a window of versions that callers were told resulted in
|
||||
# TooOld.
|
||||
with DebugConflictSet() as cs:
|
||||
cs.addWrites(5, write(b"k"))
|
||||
cs.setOldestVersion(5)
|
||||
# Valid: `2 >= 0` and `2 <= newestVersion == 5`. oldestVersion stays 5,
|
||||
# so 3 and 4 are still TooOld.
|
||||
cs.setOldestVersion(2)
|
||||
assert cs.check(read(3, b"k")) == [Result.TOO_OLD]
|
||||
assert cs.check(read(4, b"k")) == [Result.TOO_OLD]
|
||||
assert cs.check(read(5, b"k")) == [Result.COMMIT]
|
||||
|
||||
# Same, but with the entries at the old oldestVersion already garbage
|
||||
# collected, so lowering oldestVersion would report Commit for a version
|
||||
# that the caller was told resulted in TooOld.
|
||||
with DebugConflictSet() as cs:
|
||||
cs.addWrites(1, write(b"a"))
|
||||
cs.addWrites(2) # No writes; advances newestVersion to 2
|
||||
cs.setOldestVersion(2) # Garbage collects the write at version 1
|
||||
cs.setOldestVersion(1)
|
||||
assert cs.check(read(1, b"a")) == [Result.TOO_OLD]
|
||||
assert cs.check(read(2, b"a")) == [Result.COMMIT]
|
||||
|
||||
|
||||
def test_hash_table_setOldestVersion_empties_map():
|
||||
# Regression test for issue #78: setOldestVersion must terminate when
|
||||
# its garbage-collection pass erases every entry in the map. Each write
|
||||
@@ -222,13 +251,18 @@ def test_merge_child_node48():
|
||||
cs.addWrites(1, write(b"\x00" * 8, b"\x00" * 10))
|
||||
|
||||
|
||||
def test_fixup_256():
|
||||
def test_fixup_256_heterogeneous_versions():
|
||||
# Regression test for issue #89: recomputing the maxVersion of a Node256
|
||||
# must consider all of its children, not just those with an index < 16.
|
||||
# Here the highest-version child ("\x10\x50") has an index >= 16, and the
|
||||
# range write ends at the Node256, which erases that child from the range
|
||||
# and recomputes the Node256's maxVersion.
|
||||
with DebugConflictSet() as cs:
|
||||
cs.addWrites(0, write(bytes([1])))
|
||||
for i in range(256):
|
||||
cs.addWrites(1, write(bytes([1, i])))
|
||||
cs.addWrites(2, write(bytes([0]), bytes([1])))
|
||||
cs.check(read(0, bytes([1]), bytes([2])))
|
||||
cs.addWrites(1, write(bytes([0x10, i])))
|
||||
cs.addWrites(10, write(bytes([0x10, 0x50])))
|
||||
cs.addWrites(11, write(bytes([0x05]), bytes([0x10])))
|
||||
assert cs.check(read(5, bytes([0x10, 0x50]))) == [Result.CONFLICT]
|
||||
|
||||
|
||||
def test_large_removal_buffer():
|
||||
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Disable frequency scaling
|
||||
for i in $(seq "$(nproc)") ; do sudo cat /sys/devices/system/cpu/cpu$((i-1))/cpufreq/scaling_max_freq | sudo tee /sys/devices/system/cpu/cpu$((i-1))/cpufreq/scaling_min_freq >/dev/null ; done
|
||||
# shellcheck disable=SC2002
|
||||
for i in $(seq "$(nproc)") ; do cat /sys/devices/system/cpu/cpu$((i-1))/cpufreq/scaling_max_freq | sudo tee /sys/devices/system/cpu/cpu$((i-1))/cpufreq/scaling_min_freq >/dev/null ; done
|
||||
|
||||
cmake -S. -Bbuild -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=clang++ -G Ninja -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DCMAKE_CXX_FLAGS=-DNVALGRIND > /dev/null
|
||||
cmake --build build > /dev/null
|
||||
|
||||
Reference in new issue
Block a user