6 Commits
Author SHA1 Message Date
andrew cc4306cfe9 Bump version
CI / release (arm64, ubuntu-latest-arm64) (push) Successful in 1m49s
CI / pre-commit (push) Successful in 2m0s
CI / test (-DCMAKE_BUILD_TYPE=Debug, debug) (push) Successful in 2m11s
CI / test (-DCMAKE_CXX_FLAGS=-DUSE_64_BIT=1, 64-bit-versions) (push) Successful in 2m12s
CI / test (-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++, gcc) (push) Successful in 2m1s
CI / test (-DUSE_SIMD_FALLBACK=ON, simd-fallback) (push) Successful in 2m9s
CI / release (amd64, ubuntu-latest-amd64) (push) Successful in 4m20s
CI / coverage (push) Successful in 2m43s
2026-07-14 15:27:09 -04:00
andrew 9b7199567b Update README.md
CI / pre-commit (push) Successful in 1m59s
CI / release (arm64, ubuntu-latest-arm64) (push) Successful in 1m51s
CI / test (-DCMAKE_BUILD_TYPE=Debug, debug) (push) Successful in 2m11s
CI / test (-DCMAKE_CXX_FLAGS=-DUSE_64_BIT=1, 64-bit-versions) (push) Successful in 2m11s
CI / test (-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++, gcc) (push) Successful in 2m1s
CI / test (-DUSE_SIMD_FALLBACK=ON, simd-fallback) (push) Successful in 2m10s
CI / release (amd64, ubuntu-latest-amd64) (push) Successful in 4m13s
CI / coverage (push) Successful in 2m48s
2026-07-14 14:32:08 -04:00
andrew 7c6c116713 Add new coverage to corpus 2026-07-14 14:23:03 -04:00
andrew 5c9e9603de Add GCOVR_EXCL_LINE
CI / release (arm64, ubuntu-latest-arm64) (push) Successful in 1m51s
CI / pre-commit (push) Successful in 2m58s
CI / test (-DCMAKE_BUILD_TYPE=Debug, debug) (push) Successful in 3m32s
CI / test (-DCMAKE_CXX_FLAGS=-DUSE_64_BIT=1, 64-bit-versions) (push) Successful in 3m32s
CI / test (-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++, gcc) (push) Successful in 3m20s
CI / test (-DUSE_SIMD_FALLBACK=ON, simd-fallback) (push) Successful in 3m36s
CI / release (amd64, ubuntu-latest-amd64) (push) Successful in 6m56s
CI / coverage (push) Successful in 4m23s
This was showing as uncovered in CI for some reason
2026-07-14 13:38:15 -04:00
andrew 862b6a4886 Port CI from main
CI / release (arm64, ubuntu-latest-arm64) (push) Successful in 1m51s
CI / pre-commit (push) Successful in 2m59s
CI / test (-DCMAKE_BUILD_TYPE=Debug, debug) (push) Successful in 8m0s
CI / test (-DCMAKE_CXX_FLAGS=-DUSE_64_BIT=1, 64-bit-versions) (push) Successful in 3m34s
CI / test (-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++, gcc) (push) Successful in 3m17s
CI / test (-DUSE_SIMD_FALLBACK=ON, simd-fallback) (push) Successful in 3m30s
CI / release (amd64, ubuntu-latest-amd64) (push) Successful in 6m43s
CI / coverage (push) Failing after 3m57s
2026-07-14 12:28:08 -04:00
andrew a69dac7f16 Merge pull request 'Fix move-assignment leak and self-assignment in ConflictSet' (#55) from weaselbot/conflict-set:weaselbot/issue-54 into main
Reviewed-on: #55
2026-07-14 10:56:12 -04:00
187 changed files with 678 additions and 325 deletions
+306
View File
@@ -0,0 +1,306 @@
name: CI
on: [push, pull_request]
env:
CC: clang
CXX: clang++
jobs:
pre-commit:
runs-on: ubuntu-latest-amd64
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: /var/cache/apt/archives
key: apt-amd64-${{ hashFiles('.gitea/workflows/ci.yml') }}
- name: Install dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -y git nodejs pre-commit
- uses: actions/cache@v4
with:
path: ~/.cache/pre-commit
key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
- name: Run pre-commit
run: |
git config --global --add safe.directory "$PWD"
pre-commit run --all-files --show-diff-on-failure
test:
strategy:
fail-fast: false
matrix:
include:
- name: 64-bit-versions
cmake_args: -DCMAKE_CXX_FLAGS=-DUSE_64_BIT=1
- name: debug
cmake_args: -DCMAKE_BUILD_TYPE=Debug
- name: simd-fallback
cmake_args: -DUSE_SIMD_FALLBACK=ON
- name: gcc
cmake_args: -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++
runs-on: ubuntu-latest-amd64
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: /var/cache/apt/archives
key: apt-amd64-${{ hashFiles('.gitea/workflows/ci.yml') }}
- name: Install common dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -y \
build-essential ccache cmake gcc g++ libc6-dbg \
ninja-build python3 valgrind zstd
sudo curl -Ls "https://minio.weaselab.dev/public/$(uname -m)/mc.RELEASE.2025-08-13T08-35-41Z" \
-o /usr/local/bin/mc && sudo chmod +x /usr/local/bin/mc
- name: Install LLVM toolchain
if: matrix.name != 'gcc'
run: |
. /etc/os-release
wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc
echo "deb http://apt.llvm.org/${VERSION_CODENAME}/ llvm-toolchain-${VERSION_CODENAME}-21 main" | sudo tee /etc/apt/sources.list.d/llvm.list
sudo apt-get update -qq
sudo apt-get install -y \
clang-21 llvm-21 lld-21 mold
for tool in clang clang++ llvm-ar llvm-nm llvm-ranlib llvm-objcopy llvm-cov llvm-symbolizer lld ld.lld; do
sudo update-alternatives --install /usr/bin/${tool} ${tool} /usr/bin/${tool}-21 100
done
- name: Build
run: |
export CCACHE_DIR="$GITHUB_WORKSPACE/.ccache"
rm -rf build
cmake -S . -B build -G Ninja -DCMAKE_CXX_COMPILER_LAUNCHER=ccache ${{ matrix.cmake_args }}
ninja -C build
ccache -s
- name: Test
run: |
cd build
ctest --no-compress-output --test-output-size-passed 100000 --test-output-size-failed 100000 -T Test -j "$(nproc)" --timeout 90 > /dev/null
- name: Upload test results to MinIO
if: always()
env:
MINIO_ACCESS_KEY: ${{ secrets.MINIO_ACCESS_KEY }}
MC_HOST_minio: https://${{ secrets.MINIO_ACCESS_KEY }}:${{ secrets.MINIO_SECRET_KEY }}@minio.weaselab.dev
run: |
if [ -z "$MINIO_ACCESS_KEY" ]; then
echo "MinIO credentials not configured; skipping upload"
exit 0
fi
zstd build/Testing/*/Test.xml
mc cp build/Testing/*/Test.xml.zst "minio/jenkins/conflict-set/${{ gitea.run_number }}/${{ matrix.name }}/"
- name: Test summary
if: always()
run: |
python3 ctest_summary.py build/Testing/*/Test.xml \
--link "https://minio.weaselab.dev/jenkins/conflict-set/${{ gitea.run_number }}/${{ matrix.name }}/Test.xml.zst" \
| tee -a "$GITHUB_STEP_SUMMARY"
release:
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-latest-amd64
arch: amd64
- runner: ubuntu-latest-arm64
arch: arm64
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: /var/cache/apt/archives
key: apt-${{ matrix.arch }}-${{ hashFiles('.gitea/workflows/ci.yml') }}
- name: Install dependencies
run: |
. /etc/os-release
wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc
echo "deb http://apt.llvm.org/${VERSION_CODENAME}/ llvm-toolchain-${VERSION_CODENAME}-21 main" | sudo tee /etc/apt/sources.list.d/llvm.list
sudo apt-get update -qq
sudo apt-get install -y \
build-essential ccache clang-21 cmake devscripts \
libc6-dbg llvm-21 lld-21 mold ninja-build rpm \
valgrind zstd
sudo curl -Ls "https://minio.weaselab.dev/public/$(uname -m)/mc.RELEASE.2025-08-13T08-35-41Z" \
-o /usr/local/bin/mc && sudo chmod +x /usr/local/bin/mc
for tool in clang clang++ llvm-ar llvm-nm llvm-ranlib llvm-objcopy llvm-cov llvm-symbolizer lld ld.lld; do
sudo update-alternatives --install /usr/bin/${tool} ${tool} /usr/bin/${tool}-21 100
done
- name: Install paper build dependencies
if: matrix.arch == 'amd64'
run: |
sudo apt-get install -y \
biber latexmk \
texlive-bibtex-extra texlive-fonts-recommended \
texlive-latex-extra texlive-pictures
- uses: actions/cache@v4
with:
path: .ccache
key: ccache-release-${{ matrix.arch }}-${{ gitea.sha }}
restore-keys: |
ccache-release-${{ matrix.arch }}-
- name: Build
run: |
export CCACHE_DIR="$GITHUB_WORKSPACE/.ccache"
rm -rf build
cmake -S . -B build -G Ninja -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_FLAGS=-DNVALGRIND
ninja -C build
ccache -s
- name: Test
run: |
cd build
# On arm64, valgrind needs the MAKE_MEM_DEFINED client requests for
# https://git.weaselab.dev/weaselab/conflict-set/issues/39, but this
# build has -DNVALGRIND, which compiles them out. Skip valgrind
# tests here; they run annotated in the test job.
ctest --no-compress-output --test-output-size-passed 100000 --test-output-size-failed 100000 ${{ matrix.arch == 'arm64' && '-E valgrind' || '' }} -T Test -j "$(nproc)" --timeout 90 > /dev/null
- name: Package
run: |
cd build
cpack -G DEB
cpack -G RPM
- name: Build paper
if: matrix.arch == 'amd64'
run: |
cd paper
make
- name: Upload artifacts to MinIO
if: always()
shell: bash
env:
MINIO_ACCESS_KEY: ${{ secrets.MINIO_ACCESS_KEY }}
MC_HOST_minio: https://${{ secrets.MINIO_ACCESS_KEY }}:${{ secrets.MINIO_SECRET_KEY }}@minio.weaselab.dev
run: |
if [ -z "$MINIO_ACCESS_KEY" ]; then
echo "MinIO credentials not configured; skipping upload"
exit 0
fi
dest="minio/jenkins/conflict-set/${{ gitea.run_number }}/release-${{ matrix.arch }}/"
zstd build/Testing/*/Test.xml
mc cp build/Testing/*/Test.xml.zst "$dest"
if compgen -G "build/*.deb" > /dev/null; then
mc cp build/*.deb "$dest"
fi
if compgen -G "build/*.rpm" > /dev/null; then
mc cp build/*.rpm "$dest"
fi
if compgen -G "paper/*.pdf" > /dev/null; then
mc cp paper/*.pdf "$dest"
fi
- name: Test summary
if: always()
run: |
python3 ctest_summary.py build/Testing/*/Test.xml \
--link "https://minio.weaselab.dev/jenkins/conflict-set/${{ gitea.run_number }}/release-${{ matrix.arch }}/Test.xml.zst" \
| tee -a "$GITHUB_STEP_SUMMARY"
coverage:
runs-on: ubuntu-latest-amd64
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: /var/cache/apt/archives
key: apt-amd64-${{ hashFiles('.gitea/workflows/ci.yml') }}
- name: Install dependencies
run: |
. /etc/os-release
wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc
echo "deb http://apt.llvm.org/${VERSION_CODENAME}/ llvm-toolchain-${VERSION_CODENAME}-21 main" | sudo tee /etc/apt/sources.list.d/llvm.list
sudo apt-get update -qq
sudo apt-get install -y \
build-essential ccache clang-21 cmake gcovr \
libc6-dbg llvm-21 lld-21 mold ninja-build python3 valgrind zstd
sudo curl -Ls "https://minio.weaselab.dev/public/$(uname -m)/mc.RELEASE.2025-08-13T08-35-41Z" \
-o /usr/local/bin/mc && sudo chmod +x /usr/local/bin/mc
for tool in clang clang++ llvm-ar llvm-nm llvm-ranlib llvm-objcopy llvm-cov llvm-symbolizer lld ld.lld; do
sudo update-alternatives --install /usr/bin/${tool} ${tool} /usr/bin/${tool}-21 100
done
- uses: actions/cache@v4
with:
path: .ccache
key: ccache-coverage-${{ gitea.sha }}
restore-keys: |
ccache-coverage-
- name: Build
run: |
export CCACHE_DIR="$GITHUB_WORKSPACE/.ccache"
rm -rf build
cmake -S . -B build -G Ninja -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
-DCMAKE_C_FLAGS=--coverage -DCMAKE_CXX_FLAGS=--coverage \
-DCMAKE_BUILD_TYPE=Debug -DDISABLE_TSAN=ON
ninja -C build
ccache -s
- name: Test
run: |
cd build
ctest --no-compress-output --test-output-size-passed 100000 --test-output-size-failed 100000 -T Test -j "$(nproc)" --timeout 90 > /dev/null
- name: Coverage report
shell: bash
run: |
gcov_args=(-f ConflictSet.cpp -f LongestCommonPrefix.h -f Metrics.h
--gcov-executable "llvm-cov gcov" --exclude-noncode-lines)
gcovr "${gcov_args[@]}" --cobertura > build/coverage.xml
gcovr "${gcov_args[@]}"
mkdir -p build/coverage_html
gcovr "${gcov_args[@]}" --html-details build/coverage_html/index.html
gcovr "${gcov_args[@]}" --fail-under-line 100 > /dev/null
- name: Upload results to MinIO
if: always()
env:
MINIO_ACCESS_KEY: ${{ secrets.MINIO_ACCESS_KEY }}
MC_HOST_minio: https://${{ secrets.MINIO_ACCESS_KEY }}:${{ secrets.MINIO_SECRET_KEY }}@minio.weaselab.dev
run: |
if [ -z "$MINIO_ACCESS_KEY" ]; then
echo "MinIO credentials not configured; skipping upload"
exit 0
fi
dest="minio/jenkins/conflict-set/${{ gitea.run_number }}/coverage/"
zstd build/Testing/*/Test.xml
mc cp build/Testing/*/Test.xml.zst "$dest"
if [ -e build/coverage.xml ]; then
mc cp build/coverage.xml "$dest"
fi
if [ -d build/coverage_html ]; then
mc cp -r build/coverage_html "$dest"
fi
- name: Test summary
if: always()
run: |
python3 ctest_summary.py build/Testing/*/Test.xml \
--link "https://minio.weaselab.dev/jenkins/conflict-set/${{ gitea.run_number }}/coverage/Test.xml.zst" \
| tee -a "$GITHUB_STEP_SUMMARY"
echo "" | tee -a "$GITHUB_STEP_SUMMARY"
echo "📊 [Coverage report](https://minio.weaselab.dev/jenkins/conflict-set/${{ gitea.run_number }}/coverage/coverage_html/index.html)" | tee -a "$GITHUB_STEP_SUMMARY"
+6
View File
@@ -72,6 +72,12 @@ else()
add_link_options(-Wl,--gc-sections) add_link_options(-Wl,--gc-sections)
endif() endif()
if(EMSCRIPTEN)
# https://github.com/emscripten-core/emscripten/issues/15377#issuecomment-1285167486
add_link_options(-lnodefs.js -lnoderawfs.js)
add_link_options(-s ALLOW_MEMORY_GROWTH)
endif()
if(NOT USE_SIMD_FALLBACK) if(NOT USE_SIMD_FALLBACK)
cmake_push_check_state() cmake_push_check_state()
list(APPEND CMAKE_REQUIRED_FLAGS -mavx) list(APPEND CMAKE_REQUIRED_FLAGS -mavx)
+213 -129
View File
@@ -17,9 +17,9 @@ limitations under the License.
#include "ConflictSet.h" #include "ConflictSet.h"
#include "Internal.h" #include "Internal.h"
#include "LongestCommonPrefix.h" #include "LongestCommonPrefix.h"
#include "Metrics.h"
#include <algorithm> #include <algorithm>
#include <atomic>
#include <bit> #include <bit>
#include <cassert> #include <cassert>
#include <cstddef> #include <cstddef>
@@ -578,6 +578,39 @@ static_assert(kBytesPerKey - sizeof(Node0) >= kMinNodeSurplus);
constexpr int64_t kFreeListMaxMemory = 1 << 20; constexpr int64_t kFreeListMaxMemory = 1 << 20;
struct Metric {
Metric *prev;
const char *name;
const char *help;
ConflictSet::MetricsV1::Type type;
std::atomic<int64_t> value;
protected:
Metric(ConflictSet::Impl *impl, const char *name, const char *help,
ConflictSet::MetricsV1::Type type);
};
struct Gauge : private Metric {
Gauge(ConflictSet::Impl *impl, const char *name, const char *help)
: Metric(impl, name, help, ConflictSet::MetricsV1::Gauge) {}
void set(int64_t value) {
this->value.store(value, std::memory_order_relaxed);
}
};
struct Counter : private Metric {
Counter(ConflictSet::Impl *impl, const char *name, const char *help)
: Metric(impl, name, help, ConflictSet::MetricsV1::Counter) {}
// Expensive. Accumulate locally and then call add instead of repeatedly
// calling add.
void add(int64_t value) {
assert(value >= 0);
static_assert(std::atomic<int64_t>::is_always_lock_free);
this->value.fetch_add(value, std::memory_order_relaxed);
}
};
template <class T> struct BoundedFreeListAllocator { template <class T> struct BoundedFreeListAllocator {
static_assert(sizeof(T) >= sizeof(void *)); static_assert(sizeof(T) >= sizeof(void *));
@@ -2510,19 +2543,38 @@ downLeftSpine:
namespace { namespace {
// Return true if the max version among all keys that start with key[:prefixLen] // Return true if the max version among all keys that start with key[:prefixLen]
// that are >= key is <= readVersion // that are >= key is <= readVersion
bool checkRangeLeftSide(Node *n, std::span<const uint8_t> key, int prefixLen, struct CheckRangeLeftSide {
InternalVersionT readVersion, ReadContext *tls) { CheckRangeLeftSide(Node *n, std::span<const uint8_t> key, int prefixLen,
auto remaining = key; InternalVersionT readVersion, ReadContext *tls)
: n(n), remaining(key), prefixLen(prefixLen), readVersion(readVersion),
impl(tls->impl), tls(tls) {
#if DEBUG_VERBOSE && !defined(NDEBUG)
fprintf(stderr, "Check range left side from %s for keys starting with %s\n",
printable(key).c_str(),
printable(key.subspan(0, prefixLen)).c_str());
#endif
}
Node *n;
std::span<const uint8_t> remaining;
int prefixLen;
InternalVersionT readVersion;
ConflictSet::Impl *impl;
ReadContext *tls;
int searchPathLen = 0; int searchPathLen = 0;
for (;; ++tls->range_read_iterations_accum) { bool ok;
bool step() {
if (remaining.size() == 0) { if (remaining.size() == 0) {
assert(searchPathLen >= prefixLen); assert(searchPathLen >= prefixLen);
return maxVersion(n) <= readVersion; ok = maxVersion(n) <= readVersion;
return true;
} }
if (searchPathLen >= prefixLen) { if (searchPathLen >= prefixLen) {
if (!checkMaxBetweenExclusive(n, remaining[0], 256, readVersion, tls)) { if (!checkMaxBetweenExclusive(n, remaining[0], 256, readVersion, tls)) {
return false; ok = false;
return true;
} }
} }
@@ -2532,16 +2584,18 @@ bool checkRangeLeftSide(Node *n, std::span<const uint8_t> key, int prefixLen,
if (c != nullptr) { if (c != nullptr) {
if (searchPathLen < prefixLen) { if (searchPathLen < prefixLen) {
n = c; n = c;
goto downLeftSpine; return downLeftSpine();
} }
n = c; n = c;
return maxVersion(n) <= readVersion; ok = maxVersion(n) <= readVersion;
return true;
} else { } else {
n = nextSibling(n); n = nextSibling(n);
if (n == nullptr) { if (n == nullptr) {
ok = true;
return true; return true;
} }
goto downLeftSpine; return downLeftSpine();
} }
} }
@@ -2557,18 +2611,21 @@ bool checkRangeLeftSide(Node *n, std::span<const uint8_t> key, int prefixLen,
auto c = n->partialKey()[i] <=> remaining[i]; auto c = n->partialKey()[i] <=> remaining[i];
if (c > 0) { if (c > 0) {
if (searchPathLen < prefixLen) { if (searchPathLen < prefixLen) {
goto downLeftSpine; return downLeftSpine();
} }
if (n->entryPresent && n->entry.rangeVersion > readVersion) { if (n->entryPresent && n->entry.rangeVersion > readVersion) {
return false; ok = false;
return true;
} }
return maxVersion(n) <= readVersion; ok = maxVersion(n) <= readVersion;
return true;
} else { } else {
n = nextSibling(n); n = nextSibling(n);
if (n == nullptr) { if (n == nullptr) {
ok = true;
return true; return true;
} }
goto downLeftSpine; return downLeftSpine();
} }
} }
if (commonLen == n->partialKeyLen) { if (commonLen == n->partialKeyLen) {
@@ -2577,47 +2634,83 @@ bool checkRangeLeftSide(Node *n, std::span<const uint8_t> key, int prefixLen,
} else if (n->partialKeyLen > int(remaining.size())) { } else if (n->partialKeyLen > int(remaining.size())) {
assert(searchPathLen >= prefixLen); assert(searchPathLen >= prefixLen);
if (n->entryPresent && n->entry.rangeVersion > readVersion) { if (n->entryPresent && n->entry.rangeVersion > readVersion) {
return false; ok = false;
return true;
} }
return maxVersion(n) <= readVersion; ok = maxVersion(n) <= readVersion;
}
}
if (maxV <= readVersion) {
return true; return true;
} }
} }
downLeftSpine: if (maxV <= readVersion) {
ok = true;
return true;
}
return false;
}
bool downLeftSpine() {
for (; !n->entryPresent; n = getFirstChildExists(n)) { for (; !n->entryPresent; n = getFirstChildExists(n)) {
} }
return n->entry.rangeVersion <= readVersion; ok = n->entry.rangeVersion <= readVersion;
} return true;
}
};
// Return true if the max version among all keys that start with key[:prefixLen] // Return true if the max version among all keys that start with key[:prefixLen]
// that are < key is <= readVersion // that are < key is <= readVersion
bool checkRangeRightSide(Node *n, std::span<const uint8_t> key, int prefixLen, struct CheckRangeRightSide {
InternalVersionT readVersion, ReadContext *tls) { CheckRangeRightSide(Node *n, std::span<const uint8_t> key, int prefixLen,
auto remaining = key; InternalVersionT readVersion, ReadContext *tls)
int searchPathLen = 0; : n(n), key(key), remaining(key), prefixLen(prefixLen),
readVersion(readVersion), impl(tls->impl), tls(tls) {
#if DEBUG_VERBOSE && !defined(NDEBUG)
fprintf(stderr, "Check range right side to %s for keys starting with %s\n",
printable(key).c_str(),
printable(key.subspan(0, prefixLen)).c_str());
#endif
}
Node *n;
std::span<const uint8_t> key;
std::span<const uint8_t> remaining;
int prefixLen;
InternalVersionT readVersion;
ConflictSet::Impl *impl;
ReadContext *tls;
int searchPathLen = 0;
bool ok;
bool step() {
#if DEBUG_VERBOSE && !defined(NDEBUG)
fprintf(stderr,
"Search path: %s, searchPathLen: %d, prefixLen: %d, remaining: "
"%s\n",
getSearchPathPrintable(n).c_str(), searchPathLen, prefixLen,
printable(remaining).c_str());
#endif
for (;; ++tls->range_read_iterations_accum) {
assert(searchPathLen <= int(key.size())); assert(searchPathLen <= int(key.size()));
if (remaining.size() == 0) { if (remaining.size() == 0) {
goto downLeftSpine; return downLeftSpine();
} }
if (searchPathLen >= prefixLen) { if (searchPathLen >= prefixLen) {
if (n->entryPresent && n->entry.pointVersion > readVersion) { if (n->entryPresent && n->entry.pointVersion > readVersion) {
return false; ok = false;
return true;
} }
if (!checkMaxBetweenExclusive(n, -1, remaining[0], readVersion, tls)) { if (!checkMaxBetweenExclusive(n, -1, remaining[0], readVersion, tls)) {
return false; ok = false;
return true;
} }
} }
if (searchPathLen > prefixLen && n->entryPresent && if (searchPathLen > prefixLen && n->entryPresent &&
n->entry.rangeVersion > readVersion) { n->entry.rangeVersion > readVersion) {
return false; ok = false;
return true;
} }
auto *child = getChild(n, remaining[0]); auto *child = getChild(n, remaining[0]);
@@ -2625,9 +2718,9 @@ bool checkRangeRightSide(Node *n, std::span<const uint8_t> key, int prefixLen,
auto c = getChildGeq(n, remaining[0]); auto c = getChildGeq(n, remaining[0]);
if (c != nullptr) { if (c != nullptr) {
n = c; n = c;
goto downLeftSpine; return downLeftSpine();
} else { } else {
goto backtrack; return backtrack();
} }
} }
@@ -2643,30 +2736,35 @@ bool checkRangeRightSide(Node *n, std::span<const uint8_t> key, int prefixLen,
++searchPathLen; ++searchPathLen;
auto c = n->partialKey()[i] <=> remaining[i]; auto c = n->partialKey()[i] <=> remaining[i];
if (c > 0) { if (c > 0) {
goto downLeftSpine; return downLeftSpine();
} else { } else {
if (searchPathLen > prefixLen && n->entryPresent && if (searchPathLen > prefixLen && n->entryPresent &&
n->entry.rangeVersion > readVersion) { n->entry.rangeVersion > readVersion) {
return false; ok = false;
return true;
} }
goto backtrack; return backtrack();
} }
} }
if (commonLen == n->partialKeyLen) { if (commonLen == n->partialKeyLen) {
// partial key matches // partial key matches
remaining = remaining.subspan(commonLen, remaining.size() - commonLen); remaining = remaining.subspan(commonLen, remaining.size() - commonLen);
} else if (n->partialKeyLen > int(remaining.size())) { } else if (n->partialKeyLen > int(remaining.size())) {
goto downLeftSpine; return downLeftSpine();
} }
} }
return false;
} }
backtrack:
bool backtrack() {
for (;;) { for (;;) {
// searchPathLen > prefixLen implies n is not the root // searchPathLen > prefixLen implies n is not the root
if (searchPathLen > prefixLen && maxVersion(n) > readVersion) { if (searchPathLen > prefixLen && maxVersion(n) > readVersion) {
return false; ok = false;
return true;
} }
if (n->parent == nullptr) { if (n->parent == nullptr) {
ok = true;
return true; return true;
} }
auto next = getChildGeq(n->parent, n->parentsIndex + 1); auto next = getChildGeq(n->parent, n->parentsIndex + 1);
@@ -2677,14 +2775,18 @@ backtrack:
searchPathLen -= n->partialKeyLen; searchPathLen -= n->partialKeyLen;
n = next; n = next;
searchPathLen += n->partialKeyLen; searchPathLen += n->partialKeyLen;
goto downLeftSpine; return downLeftSpine();
} }
} }
downLeftSpine: }
bool downLeftSpine() {
for (; !n->entryPresent; n = getFirstChildExists(n)) { for (; !n->entryPresent; n = getFirstChildExists(n)) {
} }
return n->entry.rangeVersion <= readVersion; ok = n->entry.rangeVersion <= readVersion;
} return true;
}
};
} // namespace } // namespace
bool checkRangeRead(Node *n, std::span<const uint8_t> begin, bool checkRangeRead(Node *n, std::span<const uint8_t> begin,
@@ -2705,8 +2807,8 @@ bool checkRangeRead(Node *n, std::span<const uint8_t> begin,
auto remaining = begin.subspan(0, lcp); auto remaining = begin.subspan(0, lcp);
Arena arena; Arena arena;
// If the common prefix isn't a prefix of any physical entry in the tree, we
// Advance down common prefix, but stay on a physical path in the tree // can go to "downLeftSpine"
for (;; ++tls->range_read_iterations_accum) { for (;; ++tls->range_read_iterations_accum) {
assert(getSearchPath(arena, n) <=> assert(getSearchPath(arena, n) <=>
begin.subspan(0, lcp - remaining.size()) == begin.subspan(0, lcp - remaining.size()) ==
@@ -2747,17 +2849,47 @@ bool checkRangeRead(Node *n, std::span<const uint8_t> begin,
lcp -= consumed; lcp -= consumed;
if (lcp == int(begin.size())) { if (lcp == int(begin.size())) {
return checkRangeRightSide(n, end, lcp, readVersion, tls); CheckRangeRightSide checkRangeRightSide{n, end, lcp, readVersion, tls};
while (!checkRangeRightSide.step())
;
return checkRangeRightSide.ok;
} }
// This makes it safe to check maxVersion within checkRangeLeftSide. If this if (!checkRangeStartsWith(n, begin.subspan(0, lcp), begin[lcp], end[lcp],
readVersion, tls)) {
return false;
}
// This makes it safe to check maxVersion within CheckRangeLeftSide. If this
// were false, then we would have returned above since lcp == begin.size(). // were false, then we would have returned above since lcp == begin.size().
assert(!(n->parent == nullptr && begin.size() == 0)); assert(!(n->parent == nullptr && begin.size() == 0));
CheckRangeLeftSide checkRangeLeftSide{n, begin, lcp + 1, readVersion, tls};
CheckRangeRightSide checkRangeRightSide{n, end, lcp + 1, readVersion, tls};
return checkRangeStartsWith(n, begin.subspan(0, lcp), begin[lcp], end[lcp], for (;;) {
readVersion, tls) && bool leftDone = checkRangeLeftSide.step();
checkRangeLeftSide(n, begin, lcp + 1, readVersion, tls) && bool rightDone = checkRangeRightSide.step();
checkRangeRightSide(n, end, lcp + 1, readVersion, tls); if (!leftDone && !rightDone) {
tls->range_read_iterations_accum += 2;
continue;
}
if (leftDone && rightDone) {
break;
} else if (leftDone) {
while (!checkRangeRightSide.step()) {
++tls->range_read_iterations_accum;
}
break;
} else {
assert(rightDone);
while (!checkRangeLeftSide.step()) {
++tls->range_read_iterations_accum;
}
}
break;
}
return checkRangeLeftSide.ok && checkRangeRightSide.ok;
} }
#ifdef __x86_64__ #ifdef __x86_64__
@@ -3110,6 +3242,7 @@ struct __attribute__((visibility("hidden"))) ConflictSet::Impl {
0) * 0) *
2; 2;
memory_bytes.set(totalBytes);
point_writes_total.add(tls.accum.point_writes); point_writes_total.add(tls.accum.point_writes);
range_writes_total.add(tls.accum.range_writes); range_writes_total.add(tls.accum.range_writes);
nodes_allocated_total.add(tls.accum.nodes_allocated); nodes_allocated_total.add(tls.accum.nodes_allocated);
@@ -3198,6 +3331,7 @@ struct __attribute__((visibility("hidden"))) ConflictSet::Impl {
#endif #endif
keyUpdates = gcScanStep(keyUpdates); keyUpdates = gcScanStep(keyUpdates);
memory_bytes.set(totalBytes);
nodes_allocated_total.add(std::exchange(tls.accum.nodes_allocated, 0)); nodes_allocated_total.add(std::exchange(tls.accum.nodes_allocated, 0));
nodes_released_total.add(std::exchange(tls.accum.nodes_released, 0)); nodes_released_total.add(std::exchange(tls.accum.nodes_released, 0));
entries_inserted_total.add(std::exchange(tls.accum.entries_inserted, 0)); entries_inserted_total.add(std::exchange(tls.accum.entries_inserted, 0));
@@ -3245,7 +3379,7 @@ struct __attribute__((visibility("hidden"))) ConflictSet::Impl {
explicit Impl(int64_t oldestVersion) { explicit Impl(int64_t oldestVersion) {
assert(oldestVersion >= 0); assert(oldestVersion >= 0);
init(oldestVersion); init(oldestVersion);
metrics = initMetrics(metricsList, metricsCount); initMetrics();
} }
~Impl() { ~Impl() {
eraseTree(root, &tls); eraseTree(root, &tls);
@@ -3268,12 +3402,23 @@ struct __attribute__((visibility("hidden"))) ConflictSet::Impl {
MetricsV1 *metrics; MetricsV1 *metrics;
int metricsCount = 0; int metricsCount = 0;
Metric *metricsList = nullptr; void initMetrics() {
metrics = (MetricsV1 *)safe_malloc(metricsCount * sizeof(metrics[0]));
for (auto [i, m] = std::make_tuple(metricsCount - 1, metricList); i >= 0;
--i, m = m->prev) {
metrics[i].name = m->name;
metrics[i].help = m->help;
metrics[i].p = m;
metrics[i].type = m->type;
}
}
Metric *metricList = nullptr;
#define GAUGE(name, help) \ #define GAUGE(name, help) \
Gauge name { metricsList, metricsCount, #name, help } Gauge name { this, #name, help }
#define COUNTER(name, help) \ #define COUNTER(name, help) \
Counter name { metricsList, metricsCount, #name, help } Counter name { this, #name, help }
// ==================== METRICS DEFINITIONS ==================== // ==================== METRICS DEFINITIONS ====================
COUNTER(point_read_total, "Total number of point reads checked"); COUNTER(point_read_total, "Total number of point reads checked");
COUNTER(point_read_short_circuit_total, COUNTER(point_read_short_circuit_total,
@@ -3339,6 +3484,13 @@ struct __attribute__((visibility("hidden"))) ConflictSet::Impl {
} }
}; };
Metric::Metric(ConflictSet::Impl *impl, const char *name, const char *help,
ConflictSet::MetricsV1::Type type)
: prev(std::exchange(impl->metricList, this)), name(name), help(help),
type(type), value(0) {
++impl->metricsCount;
}
Node *&getInTree(Node *n, ConflictSet::Impl *impl) { Node *&getInTree(Node *n, ConflictSet::Impl *impl) {
return n->parent == nullptr ? impl->root return n->parent == nullptr ? impl->root
: getChildExists(n->parent, n->parentsIndex); : getChildExists(n->parent, n->parentsIndex);
@@ -3357,7 +3509,6 @@ void internal_addWrites(ConflictSet::Impl *impl,
mallocBytesDelta = 0; mallocBytesDelta = 0;
impl->addWrites(writes, count, writeVersion); impl->addWrites(writes, count, writeVersion);
impl->totalBytes += mallocBytesDelta; impl->totalBytes += mallocBytesDelta;
impl->memory_bytes.set(impl->totalBytes);
#if SHOW_MEMORY #if SHOW_MEMORY
if (impl->totalBytes != mallocBytes) { if (impl->totalBytes != mallocBytes) {
abort(); abort();
@@ -3369,7 +3520,6 @@ void internal_setOldestVersion(ConflictSet::Impl *impl, int64_t oldestVersion) {
mallocBytesDelta = 0; mallocBytesDelta = 0;
impl->setOldestVersion(oldestVersion); impl->setOldestVersion(oldestVersion);
impl->totalBytes += mallocBytesDelta; impl->totalBytes += mallocBytesDelta;
impl->memory_bytes.set(impl->totalBytes);
#if SHOW_MEMORY #if SHOW_MEMORY
if (impl->totalBytes != mallocBytes) { if (impl->totalBytes != mallocBytes) {
abort(); abort();
@@ -3498,7 +3648,12 @@ ConflictSet::ConflictSet(ConflictSet &&other) noexcept
: impl(std::exchange(other.impl, nullptr)) {} : impl(std::exchange(other.impl, nullptr)) {}
ConflictSet &ConflictSet::operator=(ConflictSet &&other) noexcept { ConflictSet &ConflictSet::operator=(ConflictSet &&other) noexcept {
if (this != &other) {
if (impl) {
internal_destroy(impl);
}
impl = std::exchange(other.impl, nullptr); impl = std::exchange(other.impl, nullptr);
}
return *this; return *this;
} }
@@ -3923,73 +4078,6 @@ struct __attribute__((visibility("default"))) PeakPrinter {
#ifdef ENABLE_MAIN #ifdef ENABLE_MAIN
#define ANKERL_NANOBENCH_IMPLEMENT
#include "third_party/nanobench.h"
template <int kN> void benchRezero() {
static_assert(kN % 16 == 0);
ankerl::nanobench::Bench bench;
InternalVersionT vs[kN];
InternalVersionT zero;
bench.run("rezero" + std::to_string(kN), [&]() {
bench.doNotOptimizeAway(vs);
bench.doNotOptimizeAway(zero);
for (int i = 0; i < kN; i += 16) {
rezero16(vs + i, zero);
}
});
}
template <int kN> void benchScan1() {
static_assert(kN % 16 == 0);
ankerl::nanobench::Bench bench;
InternalVersionT vs[kN];
uint8_t is[kN];
uint8_t begin;
uint8_t end;
InternalVersionT v;
bench.run("scan" + std::to_string(kN), [&]() {
bench.doNotOptimizeAway(vs);
bench.doNotOptimizeAway(is);
bench.doNotOptimizeAway(begin);
bench.doNotOptimizeAway(end);
bench.doNotOptimizeAway(v);
for (int i = 0; i < kN; i += 16) {
scan16</*kAVX512=*/true>(vs + i, is + i, begin, end, v);
}
});
}
template <int kN> void benchScan2() {
static_assert(kN % 16 == 0);
ankerl::nanobench::Bench bench;
InternalVersionT vs[kN];
uint8_t is[kN];
uint8_t begin;
uint8_t end;
InternalVersionT v;
bench.run("scan" + std::to_string(kN), [&]() {
bench.doNotOptimizeAway(vs);
bench.doNotOptimizeAway(begin);
bench.doNotOptimizeAway(end);
bench.doNotOptimizeAway(v);
for (int i = 0; i < kN; i += 16) {
scan16</*kAVX512=*/true>(vs + i, begin, end, v);
}
});
}
void benchLCP(int len) {
ankerl::nanobench::Bench bench;
std::vector<uint8_t> lhs(len);
std::vector<uint8_t> rhs(len);
bench.run("lcp " + std::to_string(len), [&]() {
bench.doNotOptimizeAway(lhs);
bench.doNotOptimizeAway(rhs);
bench.doNotOptimizeAway(longestCommonPrefix(lhs.data(), rhs.data(), len));
});
}
void printTree() { void printTree() {
int64_t writeVersion = 0; int64_t writeVersion = 0;
ConflictSet::Impl cs{writeVersion}; ConflictSet::Impl cs{writeVersion};
@@ -4011,11 +4099,7 @@ void printTree() {
debugPrintDot(stdout, cs.root, &cs); debugPrintDot(stdout, cs.root, &cs);
} }
int main(void) { int main(void) { printTree(); }
for (int i = 0; i < 256; ++i) {
benchLCP(i);
}
}
#endif #endif
#ifdef ENABLE_FUZZ #ifdef ENABLE_FUZZ
+6
View File
@@ -119,7 +119,13 @@ ConflictSet::ConflictSet(ConflictSet &&other) noexcept
: impl(std::exchange(other.impl, nullptr)) {} : impl(std::exchange(other.impl, nullptr)) {}
ConflictSet &ConflictSet::operator=(ConflictSet &&other) noexcept { ConflictSet &ConflictSet::operator=(ConflictSet &&other) noexcept {
if (this != &other) {
if (impl) {
impl->~Impl();
safe_free(impl, sizeof(Impl));
}
impl = std::exchange(other.impl, nullptr); impl = std::exchange(other.impl, nullptr);
}
return *this; return *this;
} }
Vendored
+1 -1
View File
@@ -129,7 +129,7 @@ pipeline {
} }
steps { steps {
script { script {
filter_args = "-f ConflictSet.cpp -f LongestCommonPrefix.h -f Metrics.h" filter_args = "-f ConflictSet.cpp -f LongestCommonPrefix.h"
} }
CleanBuildAndTest("-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++ -DCMAKE_C_FLAGS=--coverage -DCMAKE_CXX_FLAGS=--coverage -DCMAKE_BUILD_TYPE=Debug -DDISABLE_TSAN=ON") CleanBuildAndTest("-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++ -DCMAKE_C_FLAGS=--coverage -DCMAKE_CXX_FLAGS=--coverage -DCMAKE_BUILD_TYPE=Debug -DDISABLE_TSAN=ON")
sh """ sh """
+1 -1
View File
@@ -129,7 +129,7 @@ longestCommonPrefix(const uint8_t *ap, const uint8_t *bp, int cl) {
} }
int i = 0; int i = 0;
int end; int end; // GCOVR_EXCL_LINE
// kStride * kUnrollCount at a time // kStride * kUnrollCount at a time
end = cl & ~(kStride * kUnrollFactor - 1); end = cl & ~(kStride * kUnrollFactor - 1);
-64
View File
@@ -1,64 +0,0 @@
#pragma once
#include "ConflictSet.h"
#include "Internal.h"
#include <assert.h>
#include <atomic>
#include <tuple>
struct Metric {
Metric *prev;
const char *name;
const char *help;
weaselab::ConflictSet::MetricsV1::Type type;
std::atomic<int64_t> value;
protected:
Metric(Metric *&metricList, int &metricsCount, const char *name,
const char *help, weaselab::ConflictSet::MetricsV1::Type type)
: prev(std::exchange(metricList, this)), name(name), help(help),
type(type), value(0) {
++metricsCount;
}
};
struct Gauge : private Metric {
Gauge(Metric *&metricList, int &metricsCount, const char *name,
const char *help)
: Metric(metricList, metricsCount, name, help,
weaselab::ConflictSet::MetricsV1::Gauge) {}
void set(int64_t value) {
this->value.store(value, std::memory_order_relaxed);
}
};
struct Counter : private Metric {
Counter(Metric *&metricList, int &metricsCount, const char *name,
const char *help)
: Metric(metricList, metricsCount, name, help,
weaselab::ConflictSet::MetricsV1::Counter) {}
// Expensive. Accumulate locally and then call add instead of repeatedly
// calling add.
void add(int64_t value) {
assert(value >= 0);
static_assert(std::atomic<int64_t>::is_always_lock_free);
this->value.fetch_add(value, std::memory_order_relaxed);
}
};
inline weaselab::ConflictSet::MetricsV1 *initMetrics(Metric *metricsList,
int metricsCount) {
weaselab::ConflictSet::MetricsV1 *metrics =
(weaselab::ConflictSet::MetricsV1 *)safe_malloc(metricsCount *
sizeof(metrics[0]));
for (auto [i, m] = std::make_tuple(metricsCount - 1, metricsList); i >= 0;
--i, m = m->prev) {
metrics[i].name = m->name;
metrics[i].help = m->help;
metrics[i].p = m;
metrics[i].type = m->type;
}
return metrics;
}
+34 -23
View File
@@ -2,7 +2,16 @@ A data structure for optimistic concurrency control on ranges of bitwise-lexicog
Intended as an alternative to FoundationDB's skip list. Intended as an alternative to FoundationDB's skip list.
Hardware for all benchmarks is an AMD Ryzen 9 7900 with (2x32GB) 5600MT/s CL28-34-34-89 1.35V RAM Hardware for all benchmarks is an AMD Ryzen 9 7900 with (2x32GB) 5600MT/s CL28-34-34-89 1.35V RAM.
```
$ clang++ --version
Ubuntu clang version 21.1.8 (6ubuntu1)
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/lib/llvm-21/bin
```
# Microbenchmark # Microbenchmark
@@ -10,44 +19,45 @@ Hardware for all benchmarks is an AMD Ryzen 9 7900 with (2x32GB) 5600MT/s CL28-3
| ns/op | op/s | err% | ins/op | cyc/op | IPC | bra/op | miss% | total | benchmark | ns/op | op/s | err% | ins/op | cyc/op | IPC | bra/op | miss% | total | benchmark
|--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:---------- |--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:----------
| 172.03 | 5,812,791.77 | 0.4% | 3,130.62 | 879.00 | 3.562 | 509.23 | 0.0% | 0.01 | `point reads` | 171.37 | 5,835,467.28 | 0.5% | 2,915.46 | 629.31 | 4.633 | 502.91 | 0.0% | 0.01 | `point reads`
| 167.44 | 5,972,130.71 | 0.2% | 3,065.14 | 862.27 | 3.555 | 494.30 | 0.0% | 0.01 | `prefix reads` | 168.16 | 5,946,774.62 | 0.5% | 2,859.73 | 619.03 | 4.620 | 488.59 | 0.0% | 0.01 | `prefix reads`
| 238.77 | 4,188,130.84 | 0.9% | 3,589.93 | 1,259.30 | 2.851 | 637.12 | 0.0% | 0.01 | `range reads` | 245.86 | 4,067,294.63 | 0.3% | 3,508.97 | 904.82 | 3.878 | 627.73 | 0.0% | 0.01 | `range reads`
| 424.01 | 2,358,426.70 | 0.2% | 5,620.05 | 2,242.35 | 2.506 | 854.80 | 1.7% | 0.01 | `point writes` | 381.14 | 2,623,691.76 | 0.2% | 5,127.16 | 1,402.60 | 3.655 | 830.59 | 1.7% | 0.01 | `point writes`
| 418.45 | 2,389,780.56 | 0.4% | 5,525.07 | 2,211.05 | 2.499 | 831.71 | 1.7% | 0.01 | `prefix writes` | 376.24 | 2,657,887.07 | 0.3% | 5,062.85 | 1,381.07 | 3.666 | 811.68 | 1.6% | 0.01 | `prefix writes`
| 254.87 | 3,923,568.88 | 2.6% | 3,187.01 | 1,366.50 | 2.332 | 529.11 | 2.7% | 0.02 | `range writes` | 257.09 | 3,889,688.44 | 3.8% | 3,010.09 | 949.42 | 3.170 | 521.34 | 2.9% | 0.01 | `range writes`
| 675.96 | 1,479,374.50 | 3.3% | 7,735.41 | 3,468.60 | 2.230 | 1,386.02 | 1.8% | 0.01 | `monotonic increasing point writes` | 629.71 | 1,588,026.28 | 4.0% | 6,809.60 | 2,320.32 | 2.935 | 1,247.08 | 1.3% | 0.01 | `monotonic increasing point writes`
| 137,986.20 | 7,247.10 | 0.6% | 789,752.33 | 699,462.00 | 1.129 | 144,824.14 | 0.0% | 0.01 | `worst case for radix tree` | 126,164.50 | 7,926.16 | 1.7% | 772,460.25 | 463,264.67 | 1.667 | 141,512.00 | 0.2% | 0.01 | `worst case for radix tree`
| 21.63 | 46,231,564.03 | 1.0% | 448.00 | 107.14 | 4.181 | 84.00 | 0.0% | 0.01 | `create and destroy` | 15.33 | 65,217,669.23 | 0.4% | 299.00 | 56.45 | 5.297 | 64.00 | 0.0% | 0.01 | `create and destroy`
## Radix tree (this implementation) ## Radix tree (this implementation)
| ns/op | op/s | err% | ins/op | cyc/op | IPC | bra/op | miss% | total | benchmark | ns/op | op/s | err% | ins/op | cyc/op | IPC | bra/op | miss% | total | benchmark
|--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:---------- |--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:----------
| 12.88 | 77,653,350.77 | 0.5% | 185.37 | 64.45 | 2.876 | 41.51 | 0.4% | 0.01 | `point reads` | 12.81 | 78,088,759.03 | 0.7% | 187.38 | 47.15 | 3.974 | 33.70 | 0.5% | 0.01 | `point reads`
| 14.67 | 68,179,354.49 | 0.1% | 271.44 | 73.40 | 3.698 | 53.70 | 0.3% | 0.01 | `prefix reads` | 16.77 | 59,626,501.14 | 0.4% | 277.61 | 61.74 | 4.497 | 44.76 | 0.4% | 0.01 | `prefix reads`
| 34.84 | 28,701,444.36 | 0.3% | 715.74 | 175.27 | 4.084 | 127.30 | 0.2% | 0.01 | `range reads` | 42.45 | 23,556,818.87 | 0.4% | 856.96 | 156.09 | 5.490 | 126.22 | 0.2% | 0.01 | `range reads`
| 17.12 | 58,422,988.28 | 0.2% | 314.30 | 86.11 | 3.650 | 39.82 | 0.4% | 0.01 | `point writes` | 15.76 | 63,453,196.94 | 0.3% | 249.89 | 58.04 | 4.306 | 33.21 | 0.5% | 0.01 | `point writes`
| 31.42 | 31,830,804.65 | 0.1% | 591.06 | 158.07 | 3.739 | 82.67 | 0.2% | 0.01 | `prefix writes` | 29.71 | 33,662,499.39 | 0.1% | 509.14 | 109.33 | 4.657 | 70.26 | 0.5% | 0.01 | `prefix writes`
| 37.37 | 26,759,432.70 | 2.2% | 681.98 | 188.95 | 3.609 | 96.10 | 0.1% | 0.01 | `range writes` | 35.77 | 27,956,388.03 | 0.8% | 614.18 | 131.72 | 4.663 | 85.57 | 0.1% | 0.01 | `range writes`
| 76.72 | 13,035,140.63 | 2.3% | 1,421.28 | 387.17 | 3.671 | 257.76 | 0.1% | 0.01 | `monotonic increasing point writes` | 80.13 | 12,480,456.47 | 1.5% | 1,279.04 | 294.90 | 4.337 | 234.68 | 0.1% | 0.01 | `monotonic increasing point writes`
| 297,452.00 | 3,361.89 | 0.9% | 3,508,083.00 | 1,500,834.67 | 2.337 | 727,525.33 | 0.1% | 0.01 | `worst case for radix tree` | 360,748.00 | 2,772.02 | 2.6% | 4,401,835.00 | 1,318,754.00 | 3.338 | 728,537.00 | 0.0% | 0.01 | `worst case for radix tree`
| 87.70 | 11,402,490.60 | 1.0% | 1,795.00 | 442.09 | 4.060 | 297.00 | 0.0% | 0.01 | `create and destroy` | 90.69 | 11,027,103.45 | 1.0% | 1,686.00 | 333.80 | 5.051 | 284.00 | 0.0% | 0.01 | `create and destroy`
# "Real data" test # "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. Point queries only. 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 ## skip list
``` ```
Check: 4.47891 seconds, 364.05 MB/s, Add: 4.55599 seconds, 123.058 MB/s, Gc ratio: 37.1145% Check: 4.42779 seconds, 368.253 MB/s, Add: 4.64942 seconds, 120.586 MB/s, Gc ratio: 28.8784%, Peak idle memory: 5.51735e+06
``` ```
## radix tree ## radix tree
``` ```
Check: 0.953012 seconds, 1710.94 MB/s, Add: 1.30025 seconds, 431.188 MB/s, Gc ratio: 43.9816%, Peak idle memory: 2.28375e+06 Check: 0.891163 seconds, 1829.69 MB/s, Add: 1.23064 seconds, 455.579 MB/s, Gc ratio: 42.9417%, Peak idle memory: 2.28333e+06
``` ```
## hash table ## hash table
@@ -55,5 +65,6 @@ Check: 0.953012 seconds, 1710.94 MB/s, Add: 1.30025 seconds, 431.188 MB/s, Gc ra
(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) (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.804094 seconds, 2027.81 MB/s, Add: 0.652952 seconds, 858.645 MB/s, Gc ratio: 35.3885% Check: 0.858484 seconds, 1899.33 MB/s, Add: 0.632886 seconds, 885.868 MB/s, Gc ratio: 41.252%, Peak idle memory: 0
``` ```
+1 -1
View File
@@ -21,7 +21,7 @@
std::atomic<int64_t> transactions; std::atomic<int64_t> transactions;
constexpr int kBaseSearchDepth = 115; constexpr int kBaseSearchDepth = 32;
constexpr int kWindowSize = 10000000; constexpr int kWindowSize = 10000000;
std::string numToKey(int64_t num) { std::string numToKey(int64_t num) {
+14 -86
View File
@@ -22,7 +22,6 @@
#include "ConflictSet.h" #include "ConflictSet.h"
#include "Internal.h" #include "Internal.h"
#include "Metrics.h"
#include <algorithm> #include <algorithm>
#include <span> #include <span>
@@ -435,14 +434,13 @@ public:
return result; return result;
} }
// Return number of iterations of main loop void detectConflicts(ReadConflictRange *ranges, int count,
int detectConflicts(ReadConflictRange *ranges, int count,
ConflictSet::Result *transactionConflictStatus) const { ConflictSet::Result *transactionConflictStatus) const {
const int M = 16; const int M = 16;
int nextJob[M]; int nextJob[M];
CheckMax inProgress[M]; CheckMax inProgress[M];
if (!count) if (!count)
return 0; return;
int started = std::min(M, count); int started = std::min(M, count);
for (int i = 0; i < started; i++) { for (int i = 0; i < started; i++) {
@@ -453,9 +451,8 @@ public:
int prevJob = started - 1; int prevJob = started - 1;
int job = 0; int job = 0;
int iters = 0;
// vtune: 340 parts // vtune: 340 parts
for (;; ++iters) { while (true) {
if (inProgress[job].advance()) { if (inProgress[job].advance()) {
if (started == count) { if (started == count) {
if (prevJob == job) if (prevJob == job)
@@ -471,7 +468,6 @@ public:
prevJob = job; prevJob = job;
job = nextJob[job]; job = nextJob[job];
} }
return iters;
} }
void find(const StringRef *values, Finger *results, int *temp, int count) { void find(const StringRef *values, Finger *results, int *temp, int count) {
@@ -706,27 +702,15 @@ private:
}; };
}; };
struct ReadContext {
int64_t commits_accum = 0;
int64_t conflicts_accum = 0;
int64_t too_olds_accum = 0;
int64_t check_bytes_accum = 0;
};
struct __attribute__((visibility("hidden"))) ConflictSet::Impl { struct __attribute__((visibility("hidden"))) ConflictSet::Impl {
Impl(int64_t oldestVersion) Impl(int64_t oldestVersion)
: oldestVersion(oldestVersion), newestVersion(oldestVersion), : oldestVersion(oldestVersion), newestVersion(oldestVersion),
skipList(oldestVersion) { skipList(oldestVersion) {}
metrics = initMetrics(metricsList, metricsCount);
}
~Impl() { safe_free(metrics, metricsCount * sizeof(metrics[0])); }
void check(const ConflictSet::ReadRange *reads, ConflictSet::Result *results, void check(const ConflictSet::ReadRange *reads, ConflictSet::Result *results,
int count) { int count) const {
ReadContext tls;
Arena arena; Arena arena;
auto *ranges = new (arena) ReadConflictRange[count]; auto *ranges = new (arena) ReadConflictRange[count];
for (int i = 0; i < count; ++i) { for (int i = 0; i < count; ++i) {
tls.check_bytes_accum += reads[i].begin.len + reads[i].end.len;
ranges[i].begin = {reads[i].begin.p, size_t(reads[i].begin.len)}; ranges[i].begin = {reads[i].begin.p, size_t(reads[i].begin.len)};
ranges[i].end = reads[i].end.len > 0 ranges[i].end = reads[i].end.len > 0
? StringRef{reads[i].end.p, size_t(reads[i].end.len)} ? StringRef{reads[i].end.p, size_t(reads[i].end.len)}
@@ -734,22 +718,13 @@ struct __attribute__((visibility("hidden"))) ConflictSet::Impl {
ranges[i].version = reads[i].readVersion; ranges[i].version = reads[i].readVersion;
results[i] = ConflictSet::Commit; results[i] = ConflictSet::Commit;
} }
int iters = skipList.detectConflicts(ranges, count, results); skipList.detectConflicts(ranges, count, results);
for (int i = 0; i < count; ++i) { for (int i = 0; i < count; ++i) {
if (reads[i].readVersion < oldestVersion || if (reads[i].readVersion < oldestVersion ||
reads[i].readVersion < newestVersion - 2e9) { reads[i].readVersion < newestVersion - 2e9) {
results[i] = TooOld; results[i] = TooOld;
} }
tls.commits_accum += results[i] == Commit;
tls.conflicts_accum += results[i] == Conflict;
tls.too_olds_accum += results[i] == TooOld;
} }
range_read_iterations_total.add(iters);
range_read_total.add(count);
commits_total.add(tls.commits_accum);
conflicts_total.add(tls.conflicts_accum);
too_olds_total.add(tls.too_olds_accum);
check_bytes_total.add(tls.check_bytes_accum);
} }
void addWrites(const ConflictSet::WriteRange *writes, int count, void addWrites(const ConflictSet::WriteRange *writes, int count,
@@ -813,9 +788,6 @@ struct __attribute__((visibility("hidden"))) ConflictSet::Impl {
} }
void setOldestVersion(int64_t oldestVersion) { void setOldestVersion(int64_t oldestVersion) {
// This isn't 100% accurate. It overcounts if you hit the end
gc_iterations_total.add(keyUpdates);
assert(oldestVersion >= this->oldestVersion); assert(oldestVersion >= this->oldestVersion);
this->oldestVersion = oldestVersion; this->oldestVersion = oldestVersion;
SkipList::Finger finger; SkipList::Finger finger;
@@ -830,54 +802,6 @@ struct __attribute__((visibility("hidden"))) ConflictSet::Impl {
int64_t totalBytes = 0; int64_t totalBytes = 0;
MetricsV1 *metrics;
int metricsCount = 0;
Metric *metricsList = nullptr;
#define GAUGE(name, help) \
Gauge name { metricsList, metricsCount, #name, help }
#define COUNTER(name, help) \
Counter name { metricsList, metricsCount, #name, help }
// ==================== METRICS DEFINITIONS ====================
COUNTER(range_read_total, "Total number of range reads checked");
COUNTER(range_read_iterations_total,
"Total number of iterations of the main loops for range read checks");
COUNTER(commits_total,
"Total number of checks where the result is \"commit\"");
COUNTER(conflicts_total,
"Total number of checks where the result is \"conflict\"");
COUNTER(too_olds_total,
"Total number of checks where the result is \"too old\"");
COUNTER(check_bytes_total, "Total number of key bytes checked");
GAUGE(memory_bytes, "Total number of bytes in use");
COUNTER(nodes_allocated_total,
"The total number of physical tree nodes allocated");
COUNTER(nodes_released_total,
"The total number of physical tree nodes released");
COUNTER(insert_iterations_total,
"The total number of iterations of the main loop for insertion. "
"Includes searches where the entry already existed, and so insertion "
"did not take place");
COUNTER(entries_inserted_total,
"The total number of entries inserted in the tree");
COUNTER(entries_erased_total,
"The total number of entries erased from the tree");
COUNTER(
gc_iterations_total,
"The total number of iterations of the main loop for garbage collection");
COUNTER(write_bytes_total, "Total number of key bytes in calls to addWrites");
GAUGE(oldest_version,
"The lowest version that doesn't result in \"TooOld\" for checks");
GAUGE(newest_version, "The version of the most recent call to addWrites");
// ==================== END METRICS DEFINITIONS ====================
#undef GAUGE
#undef COUNTER
void getMetricsV1(MetricsV1 **metrics, int *count) {
*metrics = this->metrics;
*count = metricsCount;
}
private: private:
int64_t keyUpdates = 0; int64_t keyUpdates = 0;
Arena removalArena; Arena removalArena;
@@ -900,7 +824,6 @@ void internal_addWrites(ConflictSet::Impl *impl,
mallocBytesDelta = 0; mallocBytesDelta = 0;
impl->addWrites(writes, count, writeVersion); impl->addWrites(writes, count, writeVersion);
impl->totalBytes += mallocBytesDelta; impl->totalBytes += mallocBytesDelta;
impl->memory_bytes.set(impl->totalBytes);
#if SHOW_MEMORY #if SHOW_MEMORY
if (impl->totalBytes != mallocBytes) { if (impl->totalBytes != mallocBytes) {
abort(); abort();
@@ -912,7 +835,6 @@ void internal_setOldestVersion(ConflictSet::Impl *impl, int64_t oldestVersion) {
mallocBytesDelta = 0; mallocBytesDelta = 0;
impl->setOldestVersion(oldestVersion); impl->setOldestVersion(oldestVersion);
impl->totalBytes += mallocBytesDelta; impl->totalBytes += mallocBytesDelta;
impl->memory_bytes.set(impl->totalBytes);
#if SHOW_MEMORY #if SHOW_MEMORY
if (impl->totalBytes != mallocBytes) { if (impl->totalBytes != mallocBytes) {
abort(); abort();
@@ -936,11 +858,12 @@ int64_t internal_getBytes(ConflictSet::Impl *impl) { return impl->totalBytes; }
void internal_getMetricsV1(ConflictSet::Impl *impl, void internal_getMetricsV1(ConflictSet::Impl *impl,
ConflictSet::MetricsV1 **metrics, int *count) { ConflictSet::MetricsV1 **metrics, int *count) {
return impl->getMetricsV1(metrics, count); *metrics = nullptr;
*count = 0;
} }
double internal_getMetricValue(const ConflictSet::MetricsV1 *metric) { double internal_getMetricValue(const ConflictSet::MetricsV1 *metric) {
return ((Metric *)metric->p)->value.load(std::memory_order_relaxed); return 0;
} }
void ConflictSet::check(const ReadRange *reads, Result *results, void ConflictSet::check(const ReadRange *reads, Result *results,
@@ -980,7 +903,12 @@ ConflictSet::ConflictSet(ConflictSet &&other) noexcept
: impl(std::exchange(other.impl, nullptr)) {} : impl(std::exchange(other.impl, nullptr)) {}
ConflictSet &ConflictSet::operator=(ConflictSet &&other) noexcept { ConflictSet &ConflictSet::operator=(ConflictSet &&other) noexcept {
if (this != &other) {
if (impl) {
internal_destroy(impl);
}
impl = std::exchange(other.impl, nullptr); impl = std::exchange(other.impl, nullptr);
}
return *this; return *this;
} }
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More