Compare commits
38
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc4306cfe9 | ||
|
|
9b7199567b | ||
|
|
7c6c116713 | ||
|
|
5c9e9603de | ||
|
|
862b6a4886 | ||
|
|
a69dac7f16 | ||
|
|
c11b4714b5 | ||
|
|
bc13094406 | ||
|
|
c9d742b696 | ||
|
|
795ae7cb01 | ||
|
|
849e2d3e5c | ||
|
|
1560037680 | ||
|
|
764c31bbc8 | ||
|
|
ee3361952a | ||
|
|
8a04e57353 | ||
|
|
7f86fdee66 | ||
|
|
442755d0a6 | ||
|
|
e15b3bb137 | ||
|
|
311794c37e | ||
|
|
dfa178ba19 | ||
|
|
a16d18edfe | ||
|
|
2b60287448 | ||
|
|
0a9ac59676 | ||
|
|
e3a77ed773 | ||
|
|
cdf9a8a7b0 | ||
|
|
305dfdd52f | ||
|
|
7261c91492 | ||
|
|
f11720f5ae | ||
|
|
e2b7298af5 | ||
|
|
8e1e344f4b | ||
|
|
3634b6a59b | ||
|
|
a3cc14c807 | ||
|
|
55b3275434 | ||
|
|
3a5b86ed9e | ||
|
|
159f2eef74 | ||
|
|
2952abe811 | ||
|
|
ce54746a4a | ||
|
|
b15959d62c |
@@ -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"
|
||||
+9
-3
@@ -1,7 +1,7 @@
|
||||
cmake_minimum_required(VERSION 3.18)
|
||||
project(
|
||||
conflict-set
|
||||
VERSION 0.0.12
|
||||
VERSION 0.0.14
|
||||
DESCRIPTION
|
||||
"A data structure for optimistic concurrency control on ranges of bitwise-lexicographically-ordered keys."
|
||||
HOMEPAGE_URL "https://git.weaselab.dev/weaselab/conflict-set"
|
||||
@@ -276,9 +276,15 @@ if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR AND BUILD_TESTING)
|
||||
|
||||
find_program(VALGRIND_EXE valgrind)
|
||||
if(VALGRIND_EXE AND NOT CMAKE_CROSSCOMPILING)
|
||||
add_test(NAME conflict_set_blackbox_valgrind
|
||||
list(LENGTH CORPUS_TESTS len)
|
||||
math(EXPR last "${len} - 1")
|
||||
set(partition_size 100)
|
||||
foreach(i RANGE 0 ${last} ${partition_size})
|
||||
list(SUBLIST CORPUS_TESTS ${i} ${partition_size} partition)
|
||||
add_test(NAME conflict_set_blackbox_valgrind_${i}
|
||||
COMMAND ${VALGRIND_EXE} --error-exitcode=99 --
|
||||
$<TARGET_FILE:driver> ${CORPUS_TESTS})
|
||||
$<TARGET_FILE:driver> ${partition})
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
# api smoke tests
|
||||
|
||||
+140
-106
@@ -87,22 +87,42 @@ constexpr int64_t kMaxCorrectVersionWindow =
|
||||
std::numeric_limits<int32_t>::max();
|
||||
static_assert(kNominalVersionWindow <= kMaxCorrectVersionWindow);
|
||||
|
||||
#ifndef USE_64_BIT
|
||||
#define USE_64_BIT 0
|
||||
#endif
|
||||
|
||||
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 {
|
||||
#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;
|
||||
#if USE_64_BIT
|
||||
static const InternalVersionT zero;
|
||||
#else
|
||||
static thread_local InternalVersionT zero;
|
||||
#endif
|
||||
|
||||
private:
|
||||
#if USE_64_BIT
|
||||
int64_t value;
|
||||
#else
|
||||
uint32_t value;
|
||||
#endif
|
||||
};
|
||||
#if USE_64_BIT
|
||||
const InternalVersionT InternalVersionT::zero{0};
|
||||
#else
|
||||
thread_local InternalVersionT InternalVersionT::zero;
|
||||
#endif
|
||||
|
||||
struct Entry {
|
||||
InternalVersionT pointVersion;
|
||||
@@ -197,7 +217,6 @@ struct Node {
|
||||
/* end section that's copied to the next node */
|
||||
|
||||
uint8_t *partialKey();
|
||||
|
||||
Type getType() const { return type; }
|
||||
int32_t getCapacity() const { return partialKeyCapacity; }
|
||||
|
||||
@@ -221,84 +240,83 @@ constexpr int kNodeCopySize =
|
||||
|
||||
struct Node0 : Node {
|
||||
constexpr static auto kType = Type_Node0;
|
||||
uint8_t *partialKey() { return (uint8_t *)(this + 1); }
|
||||
|
||||
uint8_t *partialKey() { return (uint8_t *)(this + 1); }
|
||||
void copyChildrenAndKeyFrom(const Node0 &other);
|
||||
void copyChildrenAndKeyFrom(const struct Node3 &other);
|
||||
|
||||
size_t size() const { return sizeof(Node0) + getCapacity(); }
|
||||
};
|
||||
|
||||
struct Node3 : Node {
|
||||
constexpr static auto kMaxNodes = 3;
|
||||
constexpr static auto kType = Type_Node3;
|
||||
// Sorted
|
||||
uint8_t index[kMaxNodes];
|
||||
|
||||
Node *children[kMaxNodes];
|
||||
InternalVersionT childMaxVersion[kMaxNodes];
|
||||
uint8_t *partialKey() { return (uint8_t *)(this + 1); }
|
||||
// Sorted
|
||||
uint8_t index[kMaxNodes];
|
||||
|
||||
uint8_t *partialKey() { return (uint8_t *)(this + 1); }
|
||||
void copyChildrenAndKeyFrom(const Node0 &other);
|
||||
void copyChildrenAndKeyFrom(const Node3 &other);
|
||||
void copyChildrenAndKeyFrom(const struct Node16 &other);
|
||||
|
||||
size_t size() const { return sizeof(Node3) + getCapacity(); }
|
||||
};
|
||||
|
||||
struct Node16 : Node {
|
||||
constexpr static auto kType = Type_Node16;
|
||||
constexpr static auto kMaxNodes = 16;
|
||||
// Sorted
|
||||
uint8_t index[kMaxNodes];
|
||||
|
||||
Node *children[kMaxNodes];
|
||||
InternalVersionT childMaxVersion[kMaxNodes];
|
||||
uint8_t *partialKey() { return (uint8_t *)(this + 1); }
|
||||
// Sorted
|
||||
uint8_t index[kMaxNodes];
|
||||
|
||||
uint8_t *partialKey() { return (uint8_t *)(this + 1); }
|
||||
void copyChildrenAndKeyFrom(const Node3 &other);
|
||||
void copyChildrenAndKeyFrom(const Node16 &other);
|
||||
void copyChildrenAndKeyFrom(const struct Node48 &other);
|
||||
|
||||
size_t size() const { return sizeof(Node16) + getCapacity(); }
|
||||
};
|
||||
|
||||
struct Node48 : Node {
|
||||
constexpr static auto kType = Type_Node48;
|
||||
constexpr static auto kMaxNodes = 48;
|
||||
BitSet bitSet;
|
||||
int8_t nextFree;
|
||||
int8_t index[256];
|
||||
Node *children[kMaxNodes];
|
||||
InternalVersionT childMaxVersion[kMaxNodes];
|
||||
uint8_t reverseIndex[kMaxNodes];
|
||||
constexpr static int kMaxOfMaxPageSize = 16;
|
||||
constexpr static int kMaxOfMaxShift =
|
||||
std::countr_zero(uint32_t(kMaxOfMaxPageSize));
|
||||
constexpr static int kMaxOfMaxTotalPages = kMaxNodes / kMaxOfMaxPageSize;
|
||||
|
||||
BitSet bitSet;
|
||||
Node *children[kMaxNodes];
|
||||
InternalVersionT childMaxVersion[kMaxNodes];
|
||||
InternalVersionT maxOfMax[kMaxOfMaxTotalPages];
|
||||
uint8_t reverseIndex[kMaxNodes];
|
||||
int8_t index[256];
|
||||
|
||||
uint8_t *partialKey() { return (uint8_t *)(this + 1); }
|
||||
|
||||
void copyChildrenAndKeyFrom(const Node16 &other);
|
||||
void copyChildrenAndKeyFrom(const Node48 &other);
|
||||
void copyChildrenAndKeyFrom(const struct Node256 &other);
|
||||
|
||||
size_t size() const { return sizeof(Node48) + getCapacity(); }
|
||||
};
|
||||
|
||||
struct Node256 : Node {
|
||||
constexpr static auto kType = Type_Node256;
|
||||
BitSet bitSet;
|
||||
Node *children[256];
|
||||
InternalVersionT childMaxVersion[256];
|
||||
constexpr static auto kMaxNodes = 256;
|
||||
constexpr static int kMaxOfMaxPageSize = 16;
|
||||
constexpr static int kMaxOfMaxShift =
|
||||
std::countr_zero(uint32_t(kMaxOfMaxPageSize));
|
||||
constexpr static int kMaxOfMaxTotalPages = 256 / kMaxOfMaxPageSize;
|
||||
constexpr static int kMaxOfMaxTotalPages = kMaxNodes / kMaxOfMaxPageSize;
|
||||
|
||||
BitSet bitSet;
|
||||
Node *children[kMaxNodes];
|
||||
InternalVersionT childMaxVersion[kMaxNodes];
|
||||
InternalVersionT maxOfMax[kMaxOfMaxTotalPages];
|
||||
|
||||
uint8_t *partialKey() { return (uint8_t *)(this + 1); }
|
||||
void copyChildrenAndKeyFrom(const Node48 &other);
|
||||
void copyChildrenAndKeyFrom(const Node256 &other);
|
||||
|
||||
size_t size() const { return sizeof(Node256) + getCapacity(); }
|
||||
};
|
||||
|
||||
@@ -323,7 +341,7 @@ inline void Node3::copyChildrenAndKeyFrom(const Node0 &other) {
|
||||
inline void Node3::copyChildrenAndKeyFrom(const Node3 &other) {
|
||||
memcpy((char *)this + kNodeCopyBegin, (char *)&other + kNodeCopyBegin,
|
||||
kNodeCopySize);
|
||||
memcpy(index, other.index, sizeof(*this) - sizeof(Node));
|
||||
memcpy(children, other.children, sizeof(*this) - sizeof(Node));
|
||||
memcpy(partialKey(), &other + 1, partialKeyLen);
|
||||
for (int i = 0; i < numChildren; ++i) {
|
||||
assert(children[i]->parent == &other);
|
||||
@@ -404,7 +422,6 @@ inline void Node48::copyChildrenAndKeyFrom(const Node16 &other) {
|
||||
}
|
||||
memcpy(partialKey(), &other + 1, partialKeyLen);
|
||||
bitSet.init();
|
||||
nextFree = Node16::kMaxNodes;
|
||||
int i = 0;
|
||||
for (auto x : other.index) {
|
||||
bitSet.set(x);
|
||||
@@ -424,7 +441,6 @@ inline void Node48::copyChildrenAndKeyFrom(const Node48 &other) {
|
||||
memcpy((char *)this + kNodeCopyBegin, (char *)&other + kNodeCopyBegin,
|
||||
kNodeCopySize);
|
||||
bitSet = other.bitSet;
|
||||
nextFree = other.nextFree;
|
||||
memcpy(index, other.index, sizeof(index));
|
||||
memset(children, 0, sizeof(children));
|
||||
const auto z = InternalVersionT::zero;
|
||||
@@ -451,7 +467,6 @@ inline void Node48::copyChildrenAndKeyFrom(const Node256 &other) {
|
||||
for (auto &v : childMaxVersion) {
|
||||
v = z;
|
||||
}
|
||||
nextFree = other.numChildren;
|
||||
bitSet = other.bitSet;
|
||||
int i = 0;
|
||||
bitSet.forEachSet([&](int c) {
|
||||
@@ -523,8 +538,13 @@ std::string getSearchPath(Node *n);
|
||||
// Each node with an entry present gets a budget of kBytesPerKey. Node0 always
|
||||
// has an entry present.
|
||||
// Induction hypothesis is that each node's surplus is >= kMinNodeSurplus
|
||||
#if USE_64_BIT
|
||||
constexpr int kBytesPerKey = 144;
|
||||
constexpr int kMinNodeSurplus = 104;
|
||||
#else
|
||||
constexpr int kBytesPerKey = 112;
|
||||
constexpr int kMinNodeSurplus = 80;
|
||||
#endif
|
||||
// Cound the entry itself as a child
|
||||
constexpr int kMinChildrenNode0 = 1;
|
||||
constexpr int kMinChildrenNode3 = 2;
|
||||
@@ -729,9 +749,13 @@ struct WriteContext {
|
||||
int64_t write_bytes;
|
||||
} accum;
|
||||
|
||||
#if USE_64_BIT
|
||||
static constexpr InternalVersionT zero{0};
|
||||
#else
|
||||
// Cache a copy of InternalVersionT::zero, so we don't need to do the TLS
|
||||
// lookup as often.
|
||||
InternalVersionT zero;
|
||||
#endif
|
||||
|
||||
WriteContext() { memset(&accum, 0, sizeof(accum)); }
|
||||
|
||||
@@ -773,14 +797,7 @@ private:
|
||||
BoundedFreeListAllocator<Node256> node256;
|
||||
};
|
||||
|
||||
template <class NodeT> int getNodeIndex(NodeT *self, uint8_t index) {
|
||||
static_assert(std::is_same_v<NodeT, Node3> || std::is_same_v<NodeT, Node16>);
|
||||
|
||||
// cachegrind says the plain loop is fewer instructions and more mis-predicted
|
||||
// branches. Microbenchmark says plain loop is faster. It's written in this
|
||||
// weird "generic" way though in case someday we can use the simd
|
||||
// implementation easily if we want.
|
||||
if constexpr (std::is_same_v<NodeT, Node3>) {
|
||||
int getNodeIndex(Node3 *self, uint8_t index) {
|
||||
Node3 *n = (Node3 *)self;
|
||||
for (int i = 0; i < n->numChildren; ++i) {
|
||||
if (n->index[i] == index) {
|
||||
@@ -790,6 +807,8 @@ template <class NodeT> int getNodeIndex(NodeT *self, uint8_t index) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int getNodeIndex(Node16 *self, uint8_t index) {
|
||||
|
||||
#ifdef HAS_AVX
|
||||
// Based on https://www.the-paper-trail.org/post/art-paper-notes/
|
||||
|
||||
@@ -801,7 +820,7 @@ template <class NodeT> int getNodeIndex(NodeT *self, uint8_t index) {
|
||||
// keys aren't valid, we'll mask the results to only consider the valid ones
|
||||
// below.
|
||||
__m128i indices;
|
||||
memcpy(&indices, self->index, NodeT::kMaxNodes);
|
||||
memcpy(&indices, self->index, Node16::kMaxNodes);
|
||||
__m128i results = _mm_cmpeq_epi8(key_vec, indices);
|
||||
|
||||
// Build a mask to select only the first node->num_children values from the
|
||||
@@ -824,12 +843,11 @@ template <class NodeT> int getNodeIndex(NodeT *self, uint8_t index) {
|
||||
// https://community.arm.com/arm-community-blogs/b/infrastructure-solutions-blog/posts/porting-x86-vector-bitmask-optimizations-to-arm-neon
|
||||
|
||||
uint8x16_t indices;
|
||||
memcpy(&indices, self->index, NodeT::kMaxNodes);
|
||||
memcpy(&indices, self->index, Node16::kMaxNodes);
|
||||
// 0xff for each match
|
||||
uint16x8_t results =
|
||||
vreinterpretq_u16_u8(vceqq_u8(vdupq_n_u8(index), indices));
|
||||
static_assert(NodeT::kMaxNodes <= 16);
|
||||
assume(self->numChildren <= NodeT::kMaxNodes);
|
||||
assume(self->numChildren <= Node16::kMaxNodes);
|
||||
uint64_t mask = self->numChildren == 16
|
||||
? uint64_t(-1)
|
||||
: (uint64_t(1) << (self->numChildren * 4)) - 1;
|
||||
@@ -1082,15 +1100,9 @@ ChildAndMaxVersion getChildAndMaxVersion(Node *self, uint8_t index) {
|
||||
}
|
||||
}
|
||||
|
||||
template <class NodeT> Node *getChildGeqSimd(NodeT *self, int child) {
|
||||
static_assert(std::is_same_v<NodeT, Node3> || std::is_same_v<NodeT, Node16>);
|
||||
Node *getChildGeq(Node0 *, int) { return nullptr; }
|
||||
|
||||
// cachegrind says the plain loop is fewer instructions and more mis-predicted
|
||||
// branches. Microbenchmark says plain loop is faster. It's written in this
|
||||
// weird "generic" way though so that someday we can use the simd
|
||||
// implementation easily if we want.
|
||||
if constexpr (std::is_same_v<NodeT, Node3>) {
|
||||
Node3 *n = (Node3 *)self;
|
||||
Node *getChildGeq(Node3 *n, int child) {
|
||||
for (int i = 0; i < n->numChildren; ++i) {
|
||||
if (n->index[i] >= child) {
|
||||
return n->children[i];
|
||||
@@ -1098,6 +1110,8 @@ template <class NodeT> Node *getChildGeqSimd(NodeT *self, int child) {
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Node *getChildGeq(Node16 *self, int child) {
|
||||
if (child > 255) {
|
||||
return nullptr;
|
||||
}
|
||||
@@ -1105,7 +1119,7 @@ template <class NodeT> Node *getChildGeqSimd(NodeT *self, int child) {
|
||||
#ifdef HAS_AVX
|
||||
__m128i key_vec = _mm_set1_epi8(child);
|
||||
__m128i indices;
|
||||
memcpy(&indices, self->index, NodeT::kMaxNodes);
|
||||
memcpy(&indices, self->index, Node16::kMaxNodes);
|
||||
__m128i results = _mm_cmpeq_epi8(key_vec, _mm_min_epu8(key_vec, indices));
|
||||
int mask = (1 << self->numChildren) - 1;
|
||||
uint32_t bitfield = _mm_movemask_epi8(results) & mask;
|
||||
@@ -1115,8 +1129,7 @@ template <class NodeT> Node *getChildGeqSimd(NodeT *self, int child) {
|
||||
memcpy(&indices, self->index, sizeof(self->index));
|
||||
// 0xff for each leq
|
||||
auto results = vcleq_u8(vdupq_n_u8(child), indices);
|
||||
static_assert(NodeT::kMaxNodes <= 16);
|
||||
assume(self->numChildren <= NodeT::kMaxNodes);
|
||||
assume(self->numChildren <= Node16::kMaxNodes);
|
||||
uint64_t mask = self->numChildren == 16
|
||||
? uint64_t(-1)
|
||||
: (uint64_t(1) << (self->numChildren * 4)) - 1;
|
||||
@@ -1141,13 +1154,6 @@ template <class NodeT> Node *getChildGeqSimd(NodeT *self, int child) {
|
||||
#endif
|
||||
}
|
||||
|
||||
Node *getChildGeq(Node0 *, int) { return nullptr; }
|
||||
Node *getChildGeq(Node3 *self, int child) {
|
||||
return getChildGeqSimd(self, child);
|
||||
}
|
||||
Node *getChildGeq(Node16 *self, int child) {
|
||||
return getChildGeqSimd(self, child);
|
||||
}
|
||||
Node *getChildGeq(Node48 *self, int child) {
|
||||
int c = self->bitSet.firstSetGeq(child);
|
||||
if (c < 0) {
|
||||
@@ -1360,7 +1366,7 @@ Node *&getOrCreateChild(Node *&self, std::span<const uint8_t> &key,
|
||||
auto *self3 = static_cast<Node3 *>(self);
|
||||
int i = self->numChildren - 1;
|
||||
for (; i >= 0; --i) {
|
||||
if (int(self3->index[i]) < int(index)) {
|
||||
if (self3->index[i] < index) {
|
||||
break;
|
||||
}
|
||||
self3->index[i + 1] = self3->index[i];
|
||||
@@ -1390,7 +1396,7 @@ Node *&getOrCreateChild(Node *&self, std::span<const uint8_t> &key,
|
||||
auto *self16 = static_cast<Node16 *>(self);
|
||||
int i = self->numChildren - 1;
|
||||
for (; i >= 0; --i) {
|
||||
if (int(self16->index[i]) < int(index)) {
|
||||
if (self16->index[i] < index) {
|
||||
break;
|
||||
}
|
||||
self16->index[i + 1] = self16->index[i];
|
||||
@@ -1419,9 +1425,7 @@ Node *&getOrCreateChild(Node *&self, std::span<const uint8_t> &key,
|
||||
insert48:
|
||||
auto *self48 = static_cast<Node48 *>(self);
|
||||
self48->bitSet.set(index);
|
||||
++self->numChildren;
|
||||
assert(self48->nextFree < 48);
|
||||
int nextFree = self48->nextFree++;
|
||||
auto nextFree = self48->numChildren++;
|
||||
self48->index[index] = nextFree;
|
||||
self48->reverseIndex[nextFree] = index;
|
||||
auto &result = self48->children[nextFree];
|
||||
@@ -1569,8 +1573,6 @@ void maybeDecreaseCapacity(Node *&self, WriteContext *tls,
|
||||
}
|
||||
|
||||
#if defined(HAS_AVX) && !defined(__SANITIZE_THREAD__)
|
||||
// This gets covered in local development
|
||||
// GCOVR_EXCL_START
|
||||
__attribute__((target("avx512f"))) void rezero16(InternalVersionT *vs,
|
||||
InternalVersionT zero) {
|
||||
uint32_t z;
|
||||
@@ -1580,7 +1582,6 @@ __attribute__((target("avx512f"))) void rezero16(InternalVersionT *vs,
|
||||
_mm512_sub_epi32(_mm512_loadu_epi32(vs), zvec), _mm512_setzero_epi32());
|
||||
_mm512_mask_storeu_epi32(vs, m, zvec);
|
||||
}
|
||||
// GCOVR_EXCL_STOP
|
||||
|
||||
__attribute__((target("default")))
|
||||
#endif
|
||||
@@ -1591,6 +1592,9 @@ void rezero16(InternalVersionT *vs, InternalVersionT zero) {
|
||||
}
|
||||
}
|
||||
|
||||
#if USE_64_BIT
|
||||
void rezero(Node *, InternalVersionT) {}
|
||||
#else
|
||||
void rezero(Node *n, InternalVersionT z) {
|
||||
#if DEBUG_VERBOSE && !defined(NDEBUG)
|
||||
fprintf(stderr, "rezero to %" PRId64 ": %s\n", z.toInt64(),
|
||||
@@ -1633,6 +1637,7 @@ void rezero(Node *n, InternalVersionT z) {
|
||||
__builtin_unreachable(); // GCOVR_EXCL_LINE
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void mergeWithChild(Node *&self, WriteContext *tls, ConflictSet::Impl *impl,
|
||||
Node *&dontInvalidate, Node3 *self3) {
|
||||
@@ -1812,7 +1817,7 @@ Node *erase(Node *self, WriteContext *tls, ConflictSet::Impl *impl,
|
||||
parent48->bitSet.reset(parentsIndex);
|
||||
int8_t toRemoveChildrenIndex =
|
||||
std::exchange(parent48->index[parentsIndex], -1);
|
||||
int8_t lastChildrenIndex = --parent48->nextFree;
|
||||
auto lastChildrenIndex = --parent48->numChildren;
|
||||
assert(toRemoveChildrenIndex >= 0);
|
||||
assert(lastChildrenIndex >= 0);
|
||||
if (toRemoveChildrenIndex != lastChildrenIndex) {
|
||||
@@ -1831,8 +1836,6 @@ Node *erase(Node *self, WriteContext *tls, ConflictSet::Impl *impl,
|
||||
}
|
||||
parent48->childMaxVersion[lastChildrenIndex] = tls->zero;
|
||||
|
||||
--parent->numChildren;
|
||||
|
||||
if (needsDownsize(parent48)) {
|
||||
downsize(parent48, tls, impl, result);
|
||||
}
|
||||
@@ -2017,11 +2020,18 @@ downLeftSpine:
|
||||
}
|
||||
|
||||
#ifdef HAS_AVX
|
||||
uint32_t compare16_32bit(const InternalVersionT *vs, InternalVersionT rv) {
|
||||
uint32_t compare16(const InternalVersionT *vs, InternalVersionT rv) {
|
||||
#if USE_64_BIT
|
||||
uint32_t compared = 0;
|
||||
__m128i w[4];
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
compared |= (vs[i] > rv) << i;
|
||||
}
|
||||
return compared;
|
||||
#else
|
||||
uint32_t compared = 0;
|
||||
__m128i w[4]; // GCOVR_EXCL_LINE
|
||||
memcpy(w, vs, sizeof(w));
|
||||
uint32_t r;
|
||||
uint32_t r; // GCOVR_EXCL_LINE
|
||||
memcpy(&r, &rv, sizeof(r));
|
||||
const auto rvVec = _mm_set1_epi32(r);
|
||||
const auto zero = _mm_setzero_si128();
|
||||
@@ -2031,20 +2041,28 @@ uint32_t compare16_32bit(const InternalVersionT *vs, InternalVersionT rv) {
|
||||
<< (i * 4);
|
||||
}
|
||||
return compared;
|
||||
#endif
|
||||
}
|
||||
|
||||
// This gets covered in local development
|
||||
// GCOVR_EXCL_START
|
||||
__attribute__((target("avx512f"))) uint32_t
|
||||
compare16_32bit_avx512(const InternalVersionT *vs, InternalVersionT rv) {
|
||||
compare16_avx512(const InternalVersionT *vs, InternalVersionT rv) {
|
||||
#if USE_64_BIT
|
||||
int64_t r;
|
||||
memcpy(&r, &rv, sizeof(r));
|
||||
uint32_t low =
|
||||
_mm512_cmpgt_epi64_mask(_mm512_loadu_epi64(vs), _mm512_set1_epi64(r));
|
||||
uint32_t high =
|
||||
_mm512_cmpgt_epi64_mask(_mm512_loadu_epi64(vs + 8), _mm512_set1_epi64(r));
|
||||
return low | (high << 8);
|
||||
#else
|
||||
uint32_t r;
|
||||
memcpy(&r, &rv, sizeof(r));
|
||||
return _mm512_cmpgt_epi32_mask(
|
||||
_mm512_sub_epi32(_mm512_loadu_epi32(vs), _mm512_set1_epi32(r)),
|
||||
_mm512_setzero_epi32());
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
// GCOVR_EXCL_STOP
|
||||
|
||||
// Returns true if v[i] <= readVersion for all i such that begin <= is[i] < end
|
||||
// Preconditions: begin <= end, end - begin < 256
|
||||
@@ -2099,9 +2117,9 @@ bool scan16(const InternalVersionT *vs, const uint8_t *is, int begin, int end,
|
||||
|
||||
uint32_t compared = 0;
|
||||
if constexpr (kAVX512) {
|
||||
compared = compare16_32bit_avx512(vs, readVersion); // GCOVR_EXCL_LINE
|
||||
compared = compare16_avx512(vs, readVersion);
|
||||
} else {
|
||||
compared = compare16_32bit(vs, readVersion); // GCOVR_EXCL_LINE
|
||||
compared = compare16(vs, readVersion);
|
||||
}
|
||||
return !(compared & mask);
|
||||
|
||||
@@ -2160,9 +2178,9 @@ bool scan16(const InternalVersionT *vs, int begin, int end,
|
||||
#elif defined(HAS_AVX)
|
||||
uint32_t conflict;
|
||||
if constexpr (kAVX512) {
|
||||
conflict = compare16_32bit_avx512(vs, readVersion); // GCOVR_EXCL_LINE
|
||||
conflict = compare16_avx512(vs, readVersion);
|
||||
} else {
|
||||
conflict = compare16_32bit(vs, readVersion); // GCOVR_EXCL_LINE
|
||||
conflict = compare16(vs, readVersion);
|
||||
}
|
||||
conflict &= (1 << end) - 1;
|
||||
conflict >>= begin;
|
||||
@@ -2297,12 +2315,9 @@ bool checkMaxBetweenExclusiveImpl(Node *n, int begin, int end,
|
||||
|
||||
uint32_t compared = 0;
|
||||
if constexpr (kAVX512) {
|
||||
compared = // GCOVR_EXCL_LINE
|
||||
compare16_32bit_avx512(self->childMaxVersion, // GCOVR_EXCL_LINE
|
||||
readVersion); // GCOVR_EXCL_LINE
|
||||
compared = compare16_avx512(self->childMaxVersion, readVersion);
|
||||
} else {
|
||||
compared = compare16_32bit(self->childMaxVersion,
|
||||
readVersion); // GCOVR_EXCL_LINE
|
||||
compared = compare16(self->childMaxVersion, readVersion);
|
||||
}
|
||||
return !(compared & mask) && firstRangeOk;
|
||||
|
||||
@@ -2340,10 +2355,8 @@ bool checkMaxBetweenExclusiveImpl(Node *n, int begin, int end,
|
||||
int c = self->bitSet.firstSetGeq(begin + 1);
|
||||
if (c >= 0 && c < end) {
|
||||
auto *child = self->children[self->index[c]];
|
||||
if (child->entryPresent) {
|
||||
if (!(child->entry.rangeVersion <= readVersion)) {
|
||||
if (child->entryPresent && child->entry.rangeVersion > readVersion) {
|
||||
return false;
|
||||
};
|
||||
}
|
||||
begin = c;
|
||||
} else {
|
||||
@@ -2376,10 +2389,8 @@ bool checkMaxBetweenExclusiveImpl(Node *n, int begin, int end,
|
||||
int c = self->bitSet.firstSetGeq(begin + 1);
|
||||
if (c >= 0 && c < end) {
|
||||
auto *child = self->children[c];
|
||||
if (child->entryPresent) {
|
||||
if (!(child->entry.rangeVersion <= readVersion)) {
|
||||
if (child->entryPresent && child->entry.rangeVersion > readVersion) {
|
||||
return false;
|
||||
};
|
||||
}
|
||||
begin = c;
|
||||
} else {
|
||||
@@ -2422,9 +2433,7 @@ bool checkMaxBetweenExclusiveImpl(Node *n, int begin, int end,
|
||||
}
|
||||
}
|
||||
// Check inner pages
|
||||
const int innerPageBegin = (begin >> Node256::kMaxOfMaxShift) + 1;
|
||||
const int innerPageEnd = (end - 1) >> Node256::kMaxOfMaxShift;
|
||||
return scan16<kAVX512>(self->maxOfMax, innerPageBegin, innerPageEnd,
|
||||
return scan16<kAVX512>(self->maxOfMax, firstPage + 1, lastPage,
|
||||
readVersion);
|
||||
}
|
||||
default: // GCOVR_EXCL_LINE
|
||||
@@ -2433,14 +2442,11 @@ bool checkMaxBetweenExclusiveImpl(Node *n, int begin, int end,
|
||||
}
|
||||
|
||||
#if defined(HAS_AVX) && !defined(__SANITIZE_THREAD__)
|
||||
// This gets covered in local development
|
||||
// GCOVR_EXCL_START
|
||||
__attribute__((target("avx512f"))) bool
|
||||
checkMaxBetweenExclusive(Node *n, int begin, int end,
|
||||
InternalVersionT readVersion, ReadContext *tls) {
|
||||
return checkMaxBetweenExclusiveImpl<true>(n, begin, end, readVersion, tls);
|
||||
}
|
||||
// GCOVR_EXCL_STOP
|
||||
__attribute__((target("default")))
|
||||
#endif
|
||||
bool checkMaxBetweenExclusive(Node *n, int begin, int end,
|
||||
@@ -2906,8 +2912,7 @@ checkMaxBetweenExclusiveImpl<true>(Node *n, int begin, int end,
|
||||
// of the result will have `maxVersion` set to `writeVersion` as a
|
||||
// postcondition. Nodes along the search path may be invalidated. Callers must
|
||||
// ensure that the max version of the self argument is updated.
|
||||
[[nodiscard]]
|
||||
Node **insert(Node **self, std::span<const uint8_t> key,
|
||||
[[nodiscard]] Node **insert(Node **self, std::span<const uint8_t> key,
|
||||
InternalVersionT writeVersion, WriteContext *tls) {
|
||||
|
||||
for (; key.size() != 0; ++tls->accum.insert_iterations) {
|
||||
@@ -2927,7 +2932,6 @@ void eraseTree(Node *root, WriteContext *tls) {
|
||||
tls->accum.entries_erased += n->entryPresent;
|
||||
++tls->accum.nodes_released;
|
||||
|
||||
removeNode(n);
|
||||
removeKey(n);
|
||||
|
||||
switch (n->getType()) {
|
||||
@@ -3144,6 +3148,8 @@ struct __attribute__((visibility("hidden"))) ConflictSet::Impl {
|
||||
tls.impl = this;
|
||||
int64_t check_byte_accum = 0;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
assert(reads[i].readVersion >= 0);
|
||||
assert(reads[i].readVersion <= newestVersionFullPrecision);
|
||||
const auto &r = reads[i];
|
||||
check_byte_accum += r.begin.len + r.end.len;
|
||||
auto begin = std::span<const uint8_t>(r.begin.p, r.begin.len);
|
||||
@@ -3180,10 +3186,12 @@ struct __attribute__((visibility("hidden"))) ConflictSet::Impl {
|
||||
}
|
||||
|
||||
void addWrites(const WriteRange *writes, int count, int64_t writeVersion) {
|
||||
#if !USE_64_BIT
|
||||
// There could be other conflict sets in the same thread. We need
|
||||
// InternalVersionT::zero to be correct for this conflict set for the
|
||||
// lifetime of the current call frame.
|
||||
InternalVersionT::zero = tls.zero = oldestVersion;
|
||||
#endif
|
||||
|
||||
assert(writeVersion >= newestVersionFullPrecision);
|
||||
assert(tls.accum.entries_erased == 0);
|
||||
@@ -3198,7 +3206,10 @@ struct __attribute__((visibility("hidden"))) ConflictSet::Impl {
|
||||
|
||||
newestVersionFullPrecision = writeVersion;
|
||||
newest_version.set(newestVersionFullPrecision);
|
||||
if (newestVersionFullPrecision - kNominalVersionWindow >
|
||||
oldestVersionFullPrecision) {
|
||||
setOldestVersion(newestVersionFullPrecision - kNominalVersionWindow);
|
||||
}
|
||||
while (oldestExtantVersion <
|
||||
newestVersionFullPrecision - kMaxCorrectVersionWindow) {
|
||||
gcScanStep(1000);
|
||||
@@ -3206,8 +3217,11 @@ struct __attribute__((visibility("hidden"))) ConflictSet::Impl {
|
||||
} else {
|
||||
newestVersionFullPrecision = writeVersion;
|
||||
newest_version.set(newestVersionFullPrecision);
|
||||
if (newestVersionFullPrecision - kNominalVersionWindow >
|
||||
oldestVersionFullPrecision) {
|
||||
setOldestVersion(newestVersionFullPrecision - kNominalVersionWindow);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const auto &w = writes[i];
|
||||
@@ -3291,14 +3305,22 @@ struct __attribute__((visibility("hidden"))) ConflictSet::Impl {
|
||||
return fuel;
|
||||
}
|
||||
|
||||
void setOldestVersion(int64_t o) {
|
||||
if (o <= oldestVersionFullPrecision) {
|
||||
void setOldestVersion(int64_t newOldestVersion) {
|
||||
assert(newOldestVersion >= 0);
|
||||
assert(newOldestVersion <= newestVersionFullPrecision);
|
||||
// If addWrites advances oldestVersion to keep within valid window, a
|
||||
// subsequent setOldestVersion can be legitimately called with a version
|
||||
// older than `oldestVersionFullPrecision`. < instead of <= so that we can
|
||||
// do garbage collection work without advancing the oldest version.
|
||||
if (newOldestVersion < oldestVersionFullPrecision) {
|
||||
return;
|
||||
}
|
||||
InternalVersionT oldestVersion{o};
|
||||
this->oldestVersionFullPrecision = o;
|
||||
InternalVersionT oldestVersion{newOldestVersion};
|
||||
this->oldestVersionFullPrecision = newOldestVersion;
|
||||
this->oldestVersion = oldestVersion;
|
||||
#if !USE_64_BIT
|
||||
InternalVersionT::zero = tls.zero = oldestVersion;
|
||||
#endif
|
||||
#ifdef NDEBUG
|
||||
// This is here for performance reasons, since we want to amortize the cost
|
||||
// of storing the search path as a string. In tests, we want to exercise the
|
||||
@@ -3347,12 +3369,15 @@ struct __attribute__((visibility("hidden"))) ConflictSet::Impl {
|
||||
root->entry.pointVersion = this->oldestVersion;
|
||||
root->entry.rangeVersion = this->oldestVersion;
|
||||
|
||||
#if !USE_64_BIT
|
||||
InternalVersionT::zero = tls.zero = this->oldestVersion;
|
||||
#endif
|
||||
|
||||
// Intentionally not resetting totalBytes
|
||||
}
|
||||
|
||||
explicit Impl(int64_t oldestVersion) {
|
||||
assert(oldestVersion >= 0);
|
||||
init(oldestVersion);
|
||||
initMetrics();
|
||||
}
|
||||
@@ -3623,7 +3648,12 @@ ConflictSet::ConflictSet(ConflictSet &&other) noexcept
|
||||
: impl(std::exchange(other.impl, nullptr)) {}
|
||||
|
||||
ConflictSet &ConflictSet::operator=(ConflictSet &&other) noexcept {
|
||||
if (this != &other) {
|
||||
if (impl) {
|
||||
internal_destroy(impl);
|
||||
}
|
||||
impl = std::exchange(other.impl, nullptr);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -3747,13 +3777,13 @@ std::string getSearchPath(Node *n) {
|
||||
fprintf(file,
|
||||
" k_%p [label=\"m=%" PRId64 " p=%" PRId64 " r=%" PRId64
|
||||
"\n%s\", pos=\"%d,%d!\"];\n",
|
||||
(void *)n, maxVersion(n).toInt64(),
|
||||
(void *)n, n->parent == nullptr ? -1 : maxVersion(n).toInt64(),
|
||||
n->entry.pointVersion.toInt64(),
|
||||
n->entry.rangeVersion.toInt64(),
|
||||
getPartialKeyPrintable(n).c_str(), x, y);
|
||||
} else {
|
||||
fprintf(file, " k_%p [label=\"m=%" PRId64 "\n%s\", pos=\"%d,%d!\"];\n",
|
||||
(void *)n, maxVersion(n).toInt64(),
|
||||
(void *)n, n->parent == nullptr ? -1 : maxVersion(n).toInt64(),
|
||||
getPartialKeyPrintable(n).c_str(), x, y);
|
||||
}
|
||||
x += kSeparation;
|
||||
@@ -3794,6 +3824,9 @@ Node *firstGeq(Node *n, std::string_view key) {
|
||||
n, std::span<const uint8_t>((const uint8_t *)key.data(), key.size()));
|
||||
}
|
||||
|
||||
#if USE_64_BIT
|
||||
void checkVersionsGeqOldestExtant(Node *, InternalVersionT) {}
|
||||
#else
|
||||
void checkVersionsGeqOldestExtant(Node *n,
|
||||
InternalVersionT oldestExtantVersion) {
|
||||
if (n->entryPresent) {
|
||||
@@ -3837,6 +3870,7 @@ void checkVersionsGeqOldestExtant(Node *n,
|
||||
abort();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
[[maybe_unused]] InternalVersionT
|
||||
checkMaxVersion(Node *root, Node *node, InternalVersionT oldestVersion,
|
||||
|
||||
@@ -119,7 +119,13 @@ ConflictSet::ConflictSet(ConflictSet &&other) noexcept
|
||||
: impl(std::exchange(other.impl, nullptr)) {}
|
||||
|
||||
ConflictSet &ConflictSet::operator=(ConflictSet &&other) noexcept {
|
||||
if (this != &other) {
|
||||
if (impl) {
|
||||
impl->~Impl();
|
||||
safe_free(impl, sizeof(Impl));
|
||||
}
|
||||
impl = std::exchange(other.impl, nullptr);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -20,7 +20,6 @@ using namespace weaselab;
|
||||
#include <thread>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <callgrind.h>
|
||||
|
||||
@@ -748,7 +747,10 @@ struct TestDriver {
|
||||
fprintf(stderr, "%p Set oldest version: %" PRId64 "\n", this,
|
||||
oldestVersion);
|
||||
#endif
|
||||
CALLGRIND_START_INSTRUMENTATION;
|
||||
cs.setOldestVersion(oldestVersion);
|
||||
CALLGRIND_STOP_INSTRUMENTATION;
|
||||
|
||||
if constexpr (kEnableAssertions) {
|
||||
refImpl.setOldestVersion(oldestVersion);
|
||||
}
|
||||
|
||||
Vendored
+11
@@ -48,6 +48,17 @@ pipeline {
|
||||
recordIssues(tools: [clang()])
|
||||
}
|
||||
}
|
||||
stage('64 bit versions') {
|
||||
agent {
|
||||
dockerfile {
|
||||
args '-v /home/jenkins/ccache:/ccache'
|
||||
reuseNode true
|
||||
}
|
||||
}
|
||||
steps {
|
||||
CleanBuildAndTest("-DCMAKE_CXX_FLAGS=-DUSE_64_BIT=1")
|
||||
}
|
||||
}
|
||||
stage('Debug') {
|
||||
agent {
|
||||
dockerfile {
|
||||
|
||||
@@ -129,7 +129,7 @@ longestCommonPrefix(const uint8_t *ap, const uint8_t *bp, int cl) {
|
||||
}
|
||||
|
||||
int i = 0;
|
||||
int end;
|
||||
int end; // GCOVR_EXCL_LINE
|
||||
|
||||
// kStride * kUnrollCount at a time
|
||||
end = cl & ~(kStride * kUnrollFactor - 1);
|
||||
|
||||
@@ -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.
|
||||
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:----------
|
||||
| 172.03 | 5,812,791.77 | 0.4% | 3,130.62 | 879.00 | 3.562 | 509.23 | 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`
|
||||
| 238.77 | 4,188,130.84 | 0.9% | 3,589.93 | 1,259.30 | 2.851 | 637.12 | 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`
|
||||
| 418.45 | 2,389,780.56 | 0.4% | 5,525.07 | 2,211.05 | 2.499 | 831.71 | 1.7% | 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`
|
||||
| 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`
|
||||
| 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`
|
||||
| 21.63 | 46,231,564.03 | 1.0% | 448.00 | 107.14 | 4.181 | 84.00 | 0.0% | 0.01 | `create and destroy`
|
||||
| 171.37 | 5,835,467.28 | 0.5% | 2,915.46 | 629.31 | 4.633 | 502.91 | 0.0% | 0.01 | `point reads`
|
||||
| 168.16 | 5,946,774.62 | 0.5% | 2,859.73 | 619.03 | 4.620 | 488.59 | 0.0% | 0.01 | `prefix reads`
|
||||
| 245.86 | 4,067,294.63 | 0.3% | 3,508.97 | 904.82 | 3.878 | 627.73 | 0.0% | 0.01 | `range reads`
|
||||
| 381.14 | 2,623,691.76 | 0.2% | 5,127.16 | 1,402.60 | 3.655 | 830.59 | 1.7% | 0.01 | `point 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`
|
||||
| 257.09 | 3,889,688.44 | 3.8% | 3,010.09 | 949.42 | 3.170 | 521.34 | 2.9% | 0.01 | `range 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`
|
||||
| 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`
|
||||
| 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)
|
||||
|
||||
|
||||
| ns/op | op/s | err% | ins/op | cyc/op | IPC | bra/op | miss% | total | benchmark
|
||||
|--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:----------
|
||||
| 11.04 | 90,614,308.12 | 0.8% | 180.38 | 55.13 | 3.272 | 41.51 | 0.4% | 0.01 | `point reads`
|
||||
| 14.96 | 66,843,629.12 | 0.4% | 274.41 | 74.73 | 3.672 | 55.05 | 0.3% | 0.01 | `prefix reads`
|
||||
| 37.06 | 26,982,847.61 | 0.2% | 791.04 | 185.28 | 4.269 | 142.67 | 0.2% | 0.01 | `range reads`
|
||||
| 17.89 | 55,887,365.73 | 0.6% | 335.54 | 89.79 | 3.737 | 43.84 | 0.4% | 0.01 | `point writes`
|
||||
| 31.85 | 31,394,336.65 | 0.3% | 615.32 | 159.63 | 3.855 | 87.69 | 0.2% | 0.01 | `prefix writes`
|
||||
| 36.17 | 27,647,221.45 | 0.6% | 705.11 | 182.80 | 3.857 | 100.62 | 0.1% | 0.01 | `range writes`
|
||||
| 79.01 | 12,656,457.78 | 0.7% | 1,498.35 | 402.46 | 3.723 | 270.50 | 0.1% | 0.01 | `monotonic increasing point writes`
|
||||
| 303,667.50 | 3,293.08 | 1.1% | 3,931,273.00 | 1,612,702.50 | 2.438 | 806,223.33 | 0.0% | 0.01 | `worst case for radix tree`
|
||||
| 83.70 | 11,947,443.83 | 0.7% | 1,738.03 | 429.06 | 4.051 | 270.01 | 0.0% | 0.01 | `create and destroy`
|
||||
| 12.81 | 78,088,759.03 | 0.7% | 187.38 | 47.15 | 3.974 | 33.70 | 0.5% | 0.01 | `point reads`
|
||||
| 16.77 | 59,626,501.14 | 0.4% | 277.61 | 61.74 | 4.497 | 44.76 | 0.4% | 0.01 | `prefix reads`
|
||||
| 42.45 | 23,556,818.87 | 0.4% | 856.96 | 156.09 | 5.490 | 126.22 | 0.2% | 0.01 | `range reads`
|
||||
| 15.76 | 63,453,196.94 | 0.3% | 249.89 | 58.04 | 4.306 | 33.21 | 0.5% | 0.01 | `point writes`
|
||||
| 29.71 | 33,662,499.39 | 0.1% | 509.14 | 109.33 | 4.657 | 70.26 | 0.5% | 0.01 | `prefix writes`
|
||||
| 35.77 | 27,956,388.03 | 0.8% | 614.18 | 131.72 | 4.663 | 85.57 | 0.1% | 0.01 | `range 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`
|
||||
| 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`
|
||||
| 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
|
||||
|
||||
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
|
||||
|
||||
```
|
||||
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
|
||||
|
||||
```
|
||||
Check: 0.958985 seconds, 1700.28 MB/s, Add: 1.35083 seconds, 415.044 MB/s, Gc ratio: 44.4768%, Peak idle memory: 2.33588e+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
|
||||
@@ -55,5 +65,6 @@ Check: 0.958985 seconds, 1700.28 MB/s, Add: 1.35083 seconds, 415.044 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)
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
|
||||
+92
-33
@@ -6,11 +6,15 @@
|
||||
#include <string.h>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/resource.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/uio.h>
|
||||
#include <thread>
|
||||
#include <unistd.h>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "ConflictSet.h"
|
||||
#include "third_party/nadeau.h"
|
||||
@@ -20,8 +24,8 @@ std::atomic<int64_t> transactions;
|
||||
constexpr int kBaseSearchDepth = 32;
|
||||
constexpr int kWindowSize = 10000000;
|
||||
|
||||
std::basic_string<uint8_t> numToKey(int64_t num) {
|
||||
std::basic_string<uint8_t> result;
|
||||
std::string numToKey(int64_t num) {
|
||||
std::string result;
|
||||
result.resize(kBaseSearchDepth + sizeof(int64_t));
|
||||
memset(result.data(), 0, kBaseSearchDepth);
|
||||
int64_t be = __builtin_bswap64(num);
|
||||
@@ -41,13 +45,13 @@ void workload(weaselab::ConflictSet *cs) {
|
||||
auto pointK = numToKey(pointRv);
|
||||
weaselab::ConflictSet::ReadRange reads[] = {
|
||||
{
|
||||
{pointK.data(), int(pointK.size())},
|
||||
{(const uint8_t *)pointK.data(), int(pointK.size())},
|
||||
{nullptr, 0},
|
||||
pointRv,
|
||||
},
|
||||
{
|
||||
{beginK.data(), int(beginK.size())},
|
||||
{endK.data(), int(endK.size())},
|
||||
{(const uint8_t *)beginK.data(), int(beginK.size())},
|
||||
{(const uint8_t *)endK.data(), int(endK.size())},
|
||||
version - 2,
|
||||
},
|
||||
};
|
||||
@@ -66,7 +70,7 @@ void workload(weaselab::ConflictSet *cs) {
|
||||
{
|
||||
weaselab::ConflictSet::WriteRange w;
|
||||
auto k = numToKey(version);
|
||||
w.begin.p = k.data();
|
||||
w.begin.p = (const uint8_t *)k.data();
|
||||
w.end.len = 0;
|
||||
if (version % (kWindowSize / 2) == 0) {
|
||||
for (int l = 0; l <= k.size(); ++l) {
|
||||
@@ -79,9 +83,9 @@ void workload(weaselab::ConflictSet *cs) {
|
||||
int64_t beginN = version - kWindowSize + rand() % kWindowSize;
|
||||
auto b = numToKey(beginN);
|
||||
auto e = numToKey(beginN + 1000);
|
||||
w.begin.p = b.data();
|
||||
w.begin.p = (const uint8_t *)b.data();
|
||||
w.begin.len = b.size();
|
||||
w.end.p = e.data();
|
||||
w.end.p = (const uint8_t *)e.data();
|
||||
w.end.len = e.size();
|
||||
cs->addWrites(&w, 1, version);
|
||||
}
|
||||
@@ -164,36 +168,29 @@ double toSeconds(timeval t) {
|
||||
return double(t.tv_sec) + double(t.tv_usec) * 1e-6;
|
||||
}
|
||||
|
||||
#include <linux/perf_event.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#ifdef __linux__
|
||||
#include <linux/perf_event.h>
|
||||
struct PerfCounter {
|
||||
explicit PerfCounter(int event) {
|
||||
PerfCounter(int type, int config, const std::string &labels = {},
|
||||
int groupLeaderFd = -1)
|
||||
: labels(labels) {
|
||||
struct perf_event_attr pe;
|
||||
|
||||
memset(&pe, 0, sizeof(pe));
|
||||
pe.type = PERF_TYPE_HARDWARE;
|
||||
pe.type = type;
|
||||
pe.size = sizeof(pe);
|
||||
pe.config = event;
|
||||
pe.config = config;
|
||||
pe.inherit = 1;
|
||||
pe.exclude_kernel = 1;
|
||||
pe.exclude_hv = 1;
|
||||
|
||||
fd = perf_event_open(&pe, 0, -1, -1, 0);
|
||||
if (fd == -1) {
|
||||
fprintf(stderr, "Error opening leader %llx\n", pe.config);
|
||||
exit(EXIT_FAILURE);
|
||||
fd = perf_event_open(&pe, 0, -1, groupLeaderFd, 0);
|
||||
if (fd < 0 && errno != ENOENT && errno != EINVAL) {
|
||||
perror(labels.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
int64_t total() {
|
||||
int64_t total() const {
|
||||
int64_t count;
|
||||
if (read(fd, &count, sizeof(count)) != sizeof(count)) {
|
||||
perror("read instructions from perf");
|
||||
@@ -202,10 +199,27 @@ struct PerfCounter {
|
||||
return count;
|
||||
}
|
||||
|
||||
~PerfCounter() { close(fd); }
|
||||
PerfCounter(PerfCounter &&other)
|
||||
: fd(std::exchange(other.fd, -1)), labels(std::move(other.labels)) {}
|
||||
PerfCounter &operator=(PerfCounter &&other) {
|
||||
fd = std::exchange(other.fd, -1);
|
||||
labels = std::move(other.labels);
|
||||
return *this;
|
||||
}
|
||||
|
||||
~PerfCounter() {
|
||||
if (fd >= 0) {
|
||||
close(fd);
|
||||
}
|
||||
}
|
||||
|
||||
bool ok() const { return fd >= 0; }
|
||||
const std::string &getLabels() const { return labels; }
|
||||
int getFd() const { return fd; }
|
||||
|
||||
private:
|
||||
int fd;
|
||||
std::string labels;
|
||||
static long perf_event_open(struct perf_event_attr *hw_event, pid_t pid,
|
||||
int cpu, int group_fd, unsigned long flags) {
|
||||
int ret;
|
||||
@@ -214,11 +228,6 @@ private:
|
||||
return ret;
|
||||
}
|
||||
};
|
||||
#else
|
||||
struct PerfCounter {
|
||||
explicit PerPerfCounter(int) {}
|
||||
int64_t total() { return 0; }
|
||||
};
|
||||
#endif
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
@@ -233,8 +242,50 @@ int main(int argc, char **argv) {
|
||||
int metricsCount;
|
||||
cs.getMetricsV1(&metrics, &metricsCount);
|
||||
|
||||
PerfCounter instructions{PERF_COUNT_HW_INSTRUCTIONS};
|
||||
PerfCounter cycles{PERF_COUNT_HW_CPU_CYCLES};
|
||||
#ifdef __linux__
|
||||
PerfCounter instructions{PERF_TYPE_HARDWARE, PERF_COUNT_HW_INSTRUCTIONS};
|
||||
PerfCounter cycles{PERF_TYPE_HARDWARE, PERF_COUNT_HW_CPU_CYCLES, "",
|
||||
instructions.getFd()};
|
||||
|
||||
std::vector<PerfCounter> cacheCounters;
|
||||
for (auto [id, idStr] : std::initializer_list<std::pair<int, std::string>>{
|
||||
{PERF_COUNT_HW_CACHE_L1D, "l1d"},
|
||||
{PERF_COUNT_HW_CACHE_L1I, "l1i"},
|
||||
{PERF_COUNT_HW_CACHE_LL, "ll"},
|
||||
{PERF_COUNT_HW_CACHE_DTLB, "dtlb"},
|
||||
{PERF_COUNT_HW_CACHE_ITLB, "itlb"},
|
||||
{PERF_COUNT_HW_CACHE_BPU, "bpu"},
|
||||
{PERF_COUNT_HW_CACHE_NODE, "node"},
|
||||
}) {
|
||||
for (auto [op, opStr] :
|
||||
std::initializer_list<std::pair<int, std::string>>{
|
||||
{PERF_COUNT_HW_CACHE_OP_READ, "read"},
|
||||
{PERF_COUNT_HW_CACHE_OP_WRITE, "write"},
|
||||
{PERF_COUNT_HW_CACHE_OP_PREFETCH, "prefetch"},
|
||||
}) {
|
||||
int groupLeaderFd = -1;
|
||||
for (auto [result, resultStr] :
|
||||
std::initializer_list<std::pair<int, std::string>>{
|
||||
{PERF_COUNT_HW_CACHE_RESULT_MISS, "miss"},
|
||||
{PERF_COUNT_HW_CACHE_RESULT_ACCESS, "access"},
|
||||
}) {
|
||||
auto labels = "{id=\"" + idStr + "\", op=\"" + opStr +
|
||||
"\", result=\"" + resultStr + "\"}";
|
||||
cacheCounters.emplace_back(PERF_TYPE_HW_CACHE,
|
||||
id | (op << 8) | (result << 16), labels,
|
||||
groupLeaderFd);
|
||||
if (!cacheCounters.back().ok()) {
|
||||
cacheCounters.pop_back();
|
||||
} else {
|
||||
if (groupLeaderFd == -1) {
|
||||
groupLeaderFd = cacheCounters.back().getFd();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
auto w = std::thread{workload, &cs};
|
||||
|
||||
for (;;) {
|
||||
@@ -262,6 +313,7 @@ int main(int argc, char **argv) {
|
||||
"transactions_total ";
|
||||
body += std::to_string(transactions.load(std::memory_order_relaxed));
|
||||
body += "\n";
|
||||
#ifdef __linux__
|
||||
body += "# HELP instructions_total Total number of instructions\n"
|
||||
"# TYPE instructions_total counter\n"
|
||||
"instructions_total ";
|
||||
@@ -272,6 +324,13 @@ int main(int argc, char **argv) {
|
||||
"cycles_total ";
|
||||
body += std::to_string(cycles.total());
|
||||
body += "\n";
|
||||
body += "# HELP cache_event_total Total number of cache events\n"
|
||||
"# TYPE cache_event_total counter\n";
|
||||
for (const auto &counter : cacheCounters) {
|
||||
body += "cache_event_total" + counter.getLabels() + " " +
|
||||
std::to_string(counter.total()) + "\n";
|
||||
}
|
||||
#endif
|
||||
|
||||
for (int i = 0; i < metricsCount; ++i) {
|
||||
body += "# HELP ";
|
||||
|
||||
+43
-39
@@ -25,6 +25,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
std::span<const uint8_t> keyAfter(Arena &arena, std::span<const uint8_t> key) {
|
||||
auto result =
|
||||
@@ -115,15 +116,6 @@ bool operator==(const KeyInfo &lhs, const KeyInfo &rhs) {
|
||||
return !(lhs < rhs || rhs < lhs);
|
||||
}
|
||||
|
||||
void swapSort(std::vector<KeyInfo> &points, int a, int b) {
|
||||
if (points[b] < points[a]) {
|
||||
KeyInfo temp;
|
||||
temp = points[a];
|
||||
points[a] = points[b];
|
||||
points[b] = temp;
|
||||
}
|
||||
}
|
||||
|
||||
struct SortTask {
|
||||
int begin;
|
||||
int size;
|
||||
@@ -183,13 +175,6 @@ void sortPoints(std::vector<KeyInfo> &points) {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -215,21 +200,25 @@ struct ReadConflictRange {
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
struct RandomLevel {
|
||||
explicit RandomLevel(uint32_t seed) : seed(seed) {}
|
||||
|
||||
int next() {
|
||||
int result = __builtin_clz(seed | (uint32_t(-1) >> (MaxLevels - 1)));
|
||||
seed = seed * 1664525L + 1013904223L;
|
||||
return result;
|
||||
}
|
||||
|
||||
private:
|
||||
uint32_t seed;
|
||||
};
|
||||
|
||||
class SkipList {
|
||||
private:
|
||||
RandomLevel randomLevel{0};
|
||||
|
||||
// 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.
|
||||
@@ -426,18 +415,23 @@ public:
|
||||
}
|
||||
void swap(SkipList &other) { std::swap(header, other.header); }
|
||||
|
||||
void addConflictRanges(const Finger *fingers, int rangeCount,
|
||||
// Returns the change in the number of entries
|
||||
int64_t addConflictRanges(const Finger *fingers, int rangeCount,
|
||||
Version version) {
|
||||
int64_t result = rangeCount;
|
||||
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)
|
||||
if (endF.found() == nullptr) {
|
||||
++result;
|
||||
insert(endF, endF.finger[0]->getMaxVersion(0));
|
||||
}
|
||||
|
||||
remove(startF, endF);
|
||||
result -= remove(startF, endF);
|
||||
insert(startF, version);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void detectConflicts(ReadConflictRange *ranges, int count,
|
||||
@@ -567,9 +561,10 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
void remove(const Finger &start, const Finger &end) {
|
||||
// Returns the number of entries removed
|
||||
int64_t remove(const Finger &start, const Finger &end) {
|
||||
if (start.finger[0] == end.finger[0])
|
||||
return;
|
||||
return 0;
|
||||
|
||||
Node *x = start.finger[0]->getNext(0);
|
||||
|
||||
@@ -578,17 +573,20 @@ private:
|
||||
if (start.finger[i] != end.finger[i])
|
||||
start.finger[i]->setNext(i, end.finger[i]->getNext(i));
|
||||
|
||||
int64_t result = 0;
|
||||
while (true) {
|
||||
Node *next = x->getNext(0);
|
||||
x->destroy();
|
||||
++result;
|
||||
if (x == end.finger[0])
|
||||
break;
|
||||
x = next;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void insert(const Finger &f, Version version) {
|
||||
int level = randomLevel();
|
||||
int level = randomLevel.next();
|
||||
// std::cout << std::string((const char*)value,length) << " level: " <<
|
||||
// level << std::endl;
|
||||
Node *x = Node::create(f.value, level);
|
||||
@@ -704,8 +702,6 @@ private:
|
||||
};
|
||||
};
|
||||
|
||||
struct SkipListConflictSet {};
|
||||
|
||||
struct __attribute__((visibility("hidden"))) ConflictSet::Impl {
|
||||
Impl(int64_t oldestVersion)
|
||||
: oldestVersion(oldestVersion), newestVersion(oldestVersion),
|
||||
@@ -775,17 +771,20 @@ struct __attribute__((visibility("hidden"))) ConflictSet::Impl {
|
||||
StringRef values[stripeSize];
|
||||
int64_t writeVersions[stripeSize / 2];
|
||||
int ss = stringCount - (stripes - 1) * stripeSize;
|
||||
int64_t entryDelta = 0;
|
||||
for (int s = stripes - 1; s >= 0; s--) {
|
||||
for (int i = 0; i * 2 < ss; ++i) {
|
||||
const auto &w = combinedWriteConflictRanges[s * stripeSize / 2 + i];
|
||||
values[i * 2] = w.first;
|
||||
values[i * 2 + 1] = w.second;
|
||||
keyUpdates += 3;
|
||||
}
|
||||
skipList.find(values, fingers, temp, ss);
|
||||
skipList.addConflictRanges(fingers, ss / 2, writeVersion);
|
||||
entryDelta += skipList.addConflictRanges(fingers, ss / 2, writeVersion);
|
||||
ss = stripeSize;
|
||||
}
|
||||
|
||||
// Run gc at least 200% the rate we're inserting entries
|
||||
keyUpdates += std::max<int64_t>(entryDelta, 0) * 2;
|
||||
}
|
||||
|
||||
void setOldestVersion(int64_t oldestVersion) {
|
||||
@@ -795,7 +794,7 @@ struct __attribute__((visibility("hidden"))) ConflictSet::Impl {
|
||||
int temp;
|
||||
std::span<const uint8_t> key = removalKey;
|
||||
skipList.find(&key, &finger, &temp, 1);
|
||||
skipList.removeBefore(oldestVersion, finger, std::exchange(keyUpdates, 10));
|
||||
skipList.removeBefore(oldestVersion, finger, std::exchange(keyUpdates, 0));
|
||||
removalArena = Arena();
|
||||
removalKey = copyToArena(
|
||||
removalArena, {finger.getValue().data(), finger.getValue().size()});
|
||||
@@ -804,7 +803,7 @@ struct __attribute__((visibility("hidden"))) ConflictSet::Impl {
|
||||
int64_t totalBytes = 0;
|
||||
|
||||
private:
|
||||
int64_t keyUpdates = 10;
|
||||
int64_t keyUpdates = 0;
|
||||
Arena removalArena;
|
||||
std::span<const uint8_t> removalKey;
|
||||
int64_t oldestVersion;
|
||||
@@ -904,7 +903,12 @@ ConflictSet::ConflictSet(ConflictSet &&other) noexcept
|
||||
: impl(std::exchange(other.impl, nullptr)) {}
|
||||
|
||||
ConflictSet &ConflictSet::operator=(ConflictSet &&other) noexcept {
|
||||
if (this != &other) {
|
||||
if (impl) {
|
||||
internal_destroy(impl);
|
||||
}
|
||||
impl = std::exchange(other.impl, nullptr);
|
||||
}
|
||||
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.
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
Reference in New Issue
Block a user