Compare commits
1
Commits
main
..
29ccae53c3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29ccae53c3 |
No files matched your search
+7
-29
@@ -38,43 +38,21 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- name: 64-bit-versions
|
- name: 64-bit-versions
|
||||||
runner: ubuntu-latest-amd64
|
|
||||||
arch: amd64
|
|
||||||
llvm_version: "21"
|
|
||||||
msan_url: ""
|
|
||||||
cmake_args: -DCMAKE_CXX_FLAGS=-DUSE_64_BIT=1
|
cmake_args: -DCMAKE_CXX_FLAGS=-DUSE_64_BIT=1
|
||||||
- name: debug
|
- name: debug
|
||||||
runner: ubuntu-latest-amd64
|
|
||||||
arch: amd64
|
|
||||||
llvm_version: "21"
|
|
||||||
msan_url: https://minio.weaselab.dev/public/x86_64/msan-toolchain-21.1.8.tar.zst
|
|
||||||
cmake_args: -DCMAKE_BUILD_TYPE=Debug -DMSAN_TOOLCHAIN_PATH=/opt/msan
|
cmake_args: -DCMAKE_BUILD_TYPE=Debug -DMSAN_TOOLCHAIN_PATH=/opt/msan
|
||||||
- name: simd-fallback
|
- name: simd-fallback
|
||||||
runner: ubuntu-latest-amd64
|
|
||||||
arch: amd64
|
|
||||||
llvm_version: "21"
|
|
||||||
msan_url: ""
|
|
||||||
cmake_args: -DUSE_SIMD_FALLBACK=ON
|
cmake_args: -DUSE_SIMD_FALLBACK=ON
|
||||||
- name: gcc
|
- name: gcc
|
||||||
runner: ubuntu-latest-amd64
|
|
||||||
arch: amd64
|
|
||||||
llvm_version: "21"
|
|
||||||
msan_url: ""
|
|
||||||
cmake_args: -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++
|
cmake_args: -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++
|
||||||
- name: debug-arm64
|
runs-on: ubuntu-latest-amd64
|
||||||
runner: ubuntu-latest-arm64
|
|
||||||
arch: arm64
|
|
||||||
llvm_version: "22"
|
|
||||||
msan_url: https://minio.weaselab.dev/public/aarch64/msan-toolchain-22.1.8.tar.zst
|
|
||||||
cmake_args: -DCMAKE_BUILD_TYPE=Debug -DMSAN_TOOLCHAIN_PATH=/opt/msan
|
|
||||||
runs-on: ${{ matrix.runner }}
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: /var/cache/apt/archives
|
path: /var/cache/apt/archives
|
||||||
key: apt-${{ matrix.arch }}-${{ hashFiles('.gitea/workflows/ci.yml') }}
|
key: apt-amd64-${{ hashFiles('.gitea/workflows/ci.yml') }}
|
||||||
|
|
||||||
- name: Install common dependencies
|
- name: Install common dependencies
|
||||||
run: |
|
run: |
|
||||||
@@ -90,18 +68,18 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
. /etc/os-release
|
. /etc/os-release
|
||||||
wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc
|
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}-${{ matrix.llvm_version }} main" | sudo tee /etc/apt/sources.list.d/llvm.list
|
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 update -qq
|
||||||
sudo apt-get install -y \
|
sudo apt-get install -y \
|
||||||
clang-${{ matrix.llvm_version }} llvm-${{ matrix.llvm_version }} lld-${{ matrix.llvm_version }} mold
|
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
|
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}-${{ matrix.llvm_version }} 100
|
sudo update-alternatives --install /usr/bin/${tool} ${tool} /usr/bin/${tool}-21 100
|
||||||
done
|
done
|
||||||
|
|
||||||
- name: Download MSan toolchain
|
- name: Download MSan toolchain
|
||||||
if: matrix.msan_url != ''
|
if: matrix.name == 'debug'
|
||||||
run: |
|
run: |
|
||||||
curl -Ls "${{ matrix.msan_url }}" -o /tmp/msan-toolchain.tar.zst
|
curl -Ls "https://minio.weaselab.dev/public/x86_64/msan-toolchain-21.1.8.tar.zst" -o /tmp/msan-toolchain.tar.zst
|
||||||
sudo mkdir -p /opt/msan
|
sudo mkdir -p /opt/msan
|
||||||
sudo tar --zstd -xf /tmp/msan-toolchain.tar.zst -C /opt/msan
|
sudo tar --zstd -xf /tmp/msan-toolchain.tar.zst -C /opt/msan
|
||||||
|
|
||||||
|
|||||||
+20
-30
@@ -5,7 +5,7 @@ project(
|
|||||||
DESCRIPTION
|
DESCRIPTION
|
||||||
"A data structure for optimistic concurrency control on ranges of bitwise-lexicographically-ordered keys."
|
"A data structure for optimistic concurrency control on ranges of bitwise-lexicographically-ordered keys."
|
||||||
HOMEPAGE_URL "https://git.weaselab.dev/weaselab/conflict-set"
|
HOMEPAGE_URL "https://git.weaselab.dev/weaselab/conflict-set"
|
||||||
LANGUAGES C CXX)
|
LANGUAGES C CXX ASM)
|
||||||
set(CMAKE_CXX_STANDARD 20)
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
|
|
||||||
file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/version.txt ${PROJECT_VERSION})
|
file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/version.txt ${PROJECT_VERSION})
|
||||||
@@ -130,7 +130,15 @@ endif()
|
|||||||
|
|
||||||
set(CMAKE_CXX_IMPLICIT_LINK_LIBRARIES "")
|
set(CMAKE_CXX_IMPLICIT_LINK_LIBRARIES "")
|
||||||
|
|
||||||
add_library(${PROJECT_NAME}-object OBJECT ConflictSet.cpp)
|
# Architecture-specific SIMD assembly. These functions operate on
|
||||||
|
# potentially-indeterminate memory, which is UB in C++ but well-defined in
|
||||||
|
# assembly.
|
||||||
|
set(SIMD_ASM_FILES)
|
||||||
|
if(CMAKE_SYSTEM_PROCESSOR STREQUAL x86_64 AND NOT USE_SIMD_FALLBACK)
|
||||||
|
set(SIMD_ASM_FILES ${CMAKE_CURRENT_SOURCE_DIR}/simd_x86_64.S)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_library(${PROJECT_NAME}-object OBJECT ConflictSet.cpp ${SIMD_ASM_FILES})
|
||||||
target_compile_options(${PROJECT_NAME}-object PRIVATE -fno-exceptions
|
target_compile_options(${PROJECT_NAME}-object PRIVATE -fno-exceptions
|
||||||
-fvisibility=hidden)
|
-fvisibility=hidden)
|
||||||
target_include_directories(${PROJECT_NAME}-object
|
target_include_directories(${PROJECT_NAME}-object
|
||||||
@@ -182,8 +190,6 @@ endif()
|
|||||||
|
|
||||||
include(CTest)
|
include(CTest)
|
||||||
|
|
||||||
option(TEST_SKIP_LIST "Run the slow skip list cross-check tests" OFF)
|
|
||||||
|
|
||||||
# disable tests if this is being used through e.g. FetchContent
|
# disable tests if this is being used through e.g. FetchContent
|
||||||
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR AND BUILD_TESTING)
|
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR AND BUILD_TESTING)
|
||||||
|
|
||||||
@@ -226,9 +232,8 @@ if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR AND BUILD_TESTING)
|
|||||||
target_compile_options(driver_skip_list PRIVATE ${TEST_FLAGS})
|
target_compile_options(driver_skip_list PRIVATE ${TEST_FLAGS})
|
||||||
target_link_libraries(driver_skip_list PRIVATE skip_list)
|
target_link_libraries(driver_skip_list PRIVATE skip_list)
|
||||||
|
|
||||||
# Optional cross-check of SkipList.cpp against ReferenceImpl. Opt-in via
|
# enable to test skip list
|
||||||
# -DTEST_SKIP_LIST=ON, since these are slow.
|
if(0)
|
||||||
if(TEST_SKIP_LIST)
|
|
||||||
foreach(TEST ${CORPUS_TESTS})
|
foreach(TEST ${CORPUS_TESTS})
|
||||||
get_filename_component(hash ${TEST} NAME)
|
get_filename_component(hash ${TEST} NAME)
|
||||||
add_test(NAME skip_list_${hash} COMMAND driver_skip_list ${TEST})
|
add_test(NAME skip_list_${hash} COMMAND driver_skip_list ${TEST})
|
||||||
@@ -236,7 +241,7 @@ if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR AND BUILD_TESTING)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
# ad hoc testing
|
# ad hoc testing
|
||||||
add_executable(conflict_set_main ConflictSet.cpp)
|
add_executable(conflict_set_main ConflictSet.cpp ${SIMD_ASM_FILES})
|
||||||
target_include_directories(conflict_set_main
|
target_include_directories(conflict_set_main
|
||||||
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
|
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
|
||||||
target_compile_definitions(conflict_set_main PRIVATE ENABLE_MAIN)
|
target_compile_definitions(conflict_set_main PRIVATE ENABLE_MAIN)
|
||||||
@@ -252,7 +257,7 @@ if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR AND BUILD_TESTING)
|
|||||||
cmake_pop_check_state()
|
cmake_pop_check_state()
|
||||||
|
|
||||||
if(HAS_LIB_FUZZER)
|
if(HAS_LIB_FUZZER)
|
||||||
add_executable(conflict_set_fuzz_test ConflictSet.cpp)
|
add_executable(conflict_set_fuzz_test ConflictSet.cpp ${SIMD_ASM_FILES})
|
||||||
target_include_directories(conflict_set_fuzz_test
|
target_include_directories(conflict_set_fuzz_test
|
||||||
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
|
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
|
||||||
target_compile_definitions(conflict_set_fuzz_test PRIVATE ENABLE_FUZZ)
|
target_compile_definitions(conflict_set_fuzz_test PRIVATE ENABLE_FUZZ)
|
||||||
@@ -264,7 +269,8 @@ if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR AND BUILD_TESTING)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
# whitebox tests asan+ubsan
|
# whitebox tests asan+ubsan
|
||||||
add_executable(fuzz_driver ConflictSet.cpp FuzzTestDriver.cpp)
|
add_executable(fuzz_driver ConflictSet.cpp FuzzTestDriver.cpp
|
||||||
|
${SIMD_ASM_FILES})
|
||||||
target_compile_options(fuzz_driver PRIVATE ${TEST_FLAGS})
|
target_compile_options(fuzz_driver PRIVATE ${TEST_FLAGS})
|
||||||
if(NOT CMAKE_CROSSCOMPILING)
|
if(NOT CMAKE_CROSSCOMPILING)
|
||||||
target_compile_options(fuzz_driver PRIVATE -fsanitize=address,undefined)
|
target_compile_options(fuzz_driver PRIVATE -fsanitize=address,undefined)
|
||||||
@@ -280,7 +286,8 @@ if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR AND BUILD_TESTING)
|
|||||||
|
|
||||||
# whitebox tests msan
|
# whitebox tests msan
|
||||||
if(MSAN_TOOLCHAIN_PATH)
|
if(MSAN_TOOLCHAIN_PATH)
|
||||||
add_executable(fuzz_driver_msan ConflictSet.cpp FuzzTestDriver.cpp)
|
add_executable(fuzz_driver_msan ConflictSet.cpp FuzzTestDriver.cpp
|
||||||
|
${SIMD_ASM_FILES})
|
||||||
target_compile_options(fuzz_driver_msan PRIVATE ${TEST_FLAGS})
|
target_compile_options(fuzz_driver_msan PRIVATE ${TEST_FLAGS})
|
||||||
if(NOT CMAKE_CROSSCOMPILING)
|
if(NOT CMAKE_CROSSCOMPILING)
|
||||||
target_compile_options(
|
target_compile_options(
|
||||||
@@ -308,7 +315,8 @@ if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR AND BUILD_TESTING)
|
|||||||
|
|
||||||
# tsan tests
|
# tsan tests
|
||||||
if(NOT CMAKE_CROSSCOMPILING AND NOT DISABLE_TSAN)
|
if(NOT CMAKE_CROSSCOMPILING AND NOT DISABLE_TSAN)
|
||||||
add_executable(tsan_driver ConflictSet.cpp FuzzTestDriver.cpp)
|
add_executable(tsan_driver ConflictSet.cpp FuzzTestDriver.cpp
|
||||||
|
${SIMD_ASM_FILES})
|
||||||
target_compile_options(tsan_driver PRIVATE ${TEST_FLAGS} -fsanitize=thread)
|
target_compile_options(tsan_driver PRIVATE ${TEST_FLAGS} -fsanitize=thread)
|
||||||
target_link_options(tsan_driver PRIVATE -fsanitize=thread)
|
target_link_options(tsan_driver PRIVATE -fsanitize=thread)
|
||||||
target_compile_definitions(tsan_driver PRIVATE ENABLE_FUZZ THREAD_TEST)
|
target_compile_definitions(tsan_driver PRIVATE ENABLE_FUZZ THREAD_TEST)
|
||||||
@@ -393,24 +401,6 @@ if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR AND BUILD_TESTING)
|
|||||||
PROPERTIES CXX_STANDARD_REQUIRED ON)
|
PROPERTIES CXX_STANDARD_REQUIRED ON)
|
||||||
add_test(NAME conflict_set_cxx_api_test COMMAND conflict_set_cxx_api_test)
|
add_test(NAME conflict_set_cxx_api_test COMMAND conflict_set_cxx_api_test)
|
||||||
|
|
||||||
# SHOW_MEMORY instrumentation, which the rest of the test suite doesn't
|
|
||||||
# compile with
|
|
||||||
add_executable(conflict_set_show_memory_test ConflictSet.cpp
|
|
||||||
ShowMemoryTest.cpp)
|
|
||||||
target_compile_options(conflict_set_show_memory_test PRIVATE ${TEST_FLAGS})
|
|
||||||
target_compile_definitions(conflict_set_show_memory_test
|
|
||||||
PRIVATE SHOW_MEMORY=1)
|
|
||||||
target_include_directories(conflict_set_show_memory_test
|
|
||||||
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
|
|
||||||
if(NOT CMAKE_CROSSCOMPILING)
|
|
||||||
target_compile_options(conflict_set_show_memory_test
|
|
||||||
PRIVATE -fsanitize=address,undefined)
|
|
||||||
target_link_options(conflict_set_show_memory_test PRIVATE
|
|
||||||
-fsanitize=address,undefined)
|
|
||||||
endif()
|
|
||||||
add_test(NAME conflict_set_show_memory_test
|
|
||||||
COMMAND conflict_set_show_memory_test)
|
|
||||||
|
|
||||||
# symbol visibility tests
|
# symbol visibility tests
|
||||||
if(NOT CMAKE_BUILD_TYPE STREQUAL Debug)
|
if(NOT CMAKE_BUILD_TYPE STREQUAL Debug)
|
||||||
if(APPLE)
|
if(APPLE)
|
||||||
|
|||||||
+103
-298
@@ -28,6 +28,7 @@ limitations under the License.
|
|||||||
#include "Internal.h"
|
#include "Internal.h"
|
||||||
#include "LongestCommonPrefix.h"
|
#include "LongestCommonPrefix.h"
|
||||||
#include "Metrics.h"
|
#include "Metrics.h"
|
||||||
|
#include "simd.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <bit>
|
#include <bit>
|
||||||
@@ -80,19 +81,17 @@ using namespace weaselab;
|
|||||||
#define assume assert
|
#define assume assert
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// The SHOW_MEMORY instrumentation. Declared unconditionally so that each of
|
#if SHOW_MEMORY
|
||||||
// these has a single definition in every configuration (see the definitions at
|
|
||||||
// the end of this file), rather than a no-op stub standing in for the real
|
|
||||||
// thing.
|
|
||||||
void addNode(struct Node *);
|
void addNode(struct Node *);
|
||||||
void removeNode(struct Node *);
|
void removeNode(struct Node *);
|
||||||
void addKey(struct Node *);
|
void addKey(struct Node *);
|
||||||
void removeKey(struct Node *);
|
void removeKey(struct Node *);
|
||||||
// Like `removeKey`, but with a caller-provided search path length. Used when
|
#else
|
||||||
// the node's ancestors may already have been released, which makes walking
|
constexpr void addNode(struct Node *) {}
|
||||||
// `parent` pointers invalid.
|
constexpr void removeNode(struct Node *) {}
|
||||||
void removeKey(struct Node *, int64_t searchPathLength);
|
constexpr void addKey(struct Node *) {}
|
||||||
int64_t getSearchPathLength(struct Node *);
|
constexpr void removeKey(struct Node *) {}
|
||||||
|
#endif
|
||||||
|
|
||||||
// ==================== BEGIN IMPLEMENTATION ====================
|
// ==================== BEGIN IMPLEMENTATION ====================
|
||||||
|
|
||||||
@@ -118,40 +117,13 @@ struct InternalVersionT {
|
|||||||
constexpr InternalVersionT() = default;
|
constexpr InternalVersionT() = default;
|
||||||
constexpr explicit InternalVersionT(int64_t value) : value(value) {}
|
constexpr explicit InternalVersionT(int64_t value) : value(value) {}
|
||||||
constexpr int64_t toInt64() const { return value; } // GCOVR_EXCL_LINE
|
constexpr int64_t toInt64() const { return value; } // GCOVR_EXCL_LINE
|
||||||
constexpr bool operator<(const InternalVersionT &rhs) const {
|
constexpr auto operator<=>(const InternalVersionT &rhs) const {
|
||||||
#if USE_64_BIT
|
#if USE_64_BIT
|
||||||
return value < rhs.value;
|
return value <=> rhs.value;
|
||||||
#else
|
#else
|
||||||
// Maintains ordering after overflow, as long as the full-precision versions
|
// Maintains ordering after overflow, as long as the full-precision versions
|
||||||
// are within `kMaxCorrectVersionWindow` of eachother.
|
// are within `kMaxCorrectVersionWindow` of eachother.
|
||||||
return int32_t(value - rhs.value) < 0;
|
return int32_t(value - rhs.value) <=> 0;
|
||||||
#endif
|
|
||||||
}
|
|
||||||
constexpr bool operator<=(const InternalVersionT &rhs) const {
|
|
||||||
#if USE_64_BIT
|
|
||||||
return value <= rhs.value;
|
|
||||||
#else
|
|
||||||
// Maintains ordering after overflow, as long as the full-precision versions
|
|
||||||
// are within `kMaxCorrectVersionWindow` of eachother.
|
|
||||||
return int32_t(value - rhs.value) <= 0;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
constexpr bool operator>=(const InternalVersionT &rhs) const {
|
|
||||||
#if USE_64_BIT
|
|
||||||
return value >= rhs.value;
|
|
||||||
#else
|
|
||||||
// Maintains ordering after overflow, as long as the full-precision versions
|
|
||||||
// are within `kMaxCorrectVersionWindow` of eachother.
|
|
||||||
return int32_t(value - rhs.value) >= 0;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
constexpr bool operator>(const InternalVersionT &rhs) const {
|
|
||||||
#if USE_64_BIT
|
|
||||||
return value > rhs.value;
|
|
||||||
#else
|
|
||||||
// Maintains ordering after overflow, as long as the full-precision versions
|
|
||||||
// are within `kMaxCorrectVersionWindow` of eachother.
|
|
||||||
return int32_t(value - rhs.value) > 0;
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
constexpr bool operator==(const InternalVersionT &) const = default;
|
constexpr bool operator==(const InternalVersionT &) const = default;
|
||||||
@@ -336,9 +308,7 @@ struct Node {
|
|||||||
uint8_t *partialKey();
|
uint8_t *partialKey();
|
||||||
Type getType() const { return type; }
|
Type getType() const { return type; }
|
||||||
int32_t getCapacity() const {
|
int32_t getCapacity() const {
|
||||||
// `partialKeyCapacity` is not repurposed by `deferRelease`, so this is also
|
assert(!releaseDeferred);
|
||||||
// valid for nodes with `releaseDeferred` set. It's needed to account for
|
|
||||||
// the capacity of a deferred node when it's finally released.
|
|
||||||
return partialKeyCapacity;
|
return partialKeyCapacity;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -519,7 +489,6 @@ inline void Node3::copyChildrenAndKeyFrom(const Node16 &other) {
|
|||||||
|
|
||||||
inline void Node16::copyChildrenAndKeyFrom(const Node3 &other) {
|
inline void Node16::copyChildrenAndKeyFrom(const Node3 &other) {
|
||||||
copyCommon(*this, other);
|
copyCommon(*this, other);
|
||||||
memset(index, 0, sizeof(index));
|
|
||||||
memcpy(index, other.index, Node3::kMaxNodes);
|
memcpy(index, other.index, Node3::kMaxNodes);
|
||||||
memcpy(children, other.children,
|
memcpy(children, other.children,
|
||||||
Node3::kMaxNodes * sizeof(children[0])); // NOLINT
|
Node3::kMaxNodes * sizeof(children[0])); // NOLINT
|
||||||
@@ -531,12 +500,6 @@ inline void Node16::copyChildrenAndKeyFrom(const Node3 &other) {
|
|||||||
assert(children[i]->parent == &other);
|
assert(children[i]->parent == &other);
|
||||||
children[i]->parent = this;
|
children[i]->parent = this;
|
||||||
}
|
}
|
||||||
// The index bytes beyond numChildren are initialized so that the SIMD loads
|
|
||||||
// of the full 16 bytes are well-defined (reading indeterminate bytes is UB
|
|
||||||
// in C++). valgrind then treats them as undefined so that it can still flag
|
|
||||||
// any read that relies on them.
|
|
||||||
VALGRIND_MAKE_MEM_UNDEFINED(index + numChildren,
|
|
||||||
sizeof(index) - numChildren * sizeof(index[0]));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void Node16::copyChildrenAndKeyFrom(const Node16 &other) {
|
inline void Node16::copyChildrenAndKeyFrom(const Node16 &other) {
|
||||||
@@ -553,7 +516,6 @@ inline void Node16::copyChildrenAndKeyFrom(const Node16 &other) {
|
|||||||
|
|
||||||
inline void Node16::copyChildrenAndKeyFrom(const Node48 &other) {
|
inline void Node16::copyChildrenAndKeyFrom(const Node48 &other) {
|
||||||
copyCommon(*this, other);
|
copyCommon(*this, other);
|
||||||
memset(index, 0, sizeof(index));
|
|
||||||
int i = 0;
|
int i = 0;
|
||||||
other.bitSet.forEachSet([&](int c) {
|
other.bitSet.forEachSet([&](int c) {
|
||||||
// Suppress a false positive -Waggressive-loop-optimizations warning
|
// Suppress a false positive -Waggressive-loop-optimizations warning
|
||||||
@@ -567,12 +529,6 @@ inline void Node16::copyChildrenAndKeyFrom(const Node48 &other) {
|
|||||||
++i;
|
++i;
|
||||||
});
|
});
|
||||||
memcpy(partialKey(), &other + 1, partialKeyLen);
|
memcpy(partialKey(), &other + 1, partialKeyLen);
|
||||||
// The index bytes beyond numChildren are initialized so that the SIMD loads
|
|
||||||
// of the full 16 bytes are well-defined (reading indeterminate bytes is UB
|
|
||||||
// in C++). valgrind then treats them as undefined so that it can still flag
|
|
||||||
// any read that relies on them.
|
|
||||||
VALGRIND_MAKE_MEM_UNDEFINED(index + numChildren,
|
|
||||||
sizeof(index) - numChildren * sizeof(index[0]));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void Node48::copyChildrenAndKeyFrom(const Node16 &other) {
|
inline void Node48::copyChildrenAndKeyFrom(const Node16 &other) {
|
||||||
@@ -580,7 +536,6 @@ inline void Node48::copyChildrenAndKeyFrom(const Node16 &other) {
|
|||||||
assert(numChildren == Node16::kMaxNodes);
|
assert(numChildren == Node16::kMaxNodes);
|
||||||
memset(index, -1, sizeof(index));
|
memset(index, -1, sizeof(index));
|
||||||
memset(children, 0, sizeof(children));
|
memset(children, 0, sizeof(children));
|
||||||
memset(reverseIndex, 0, sizeof(reverseIndex));
|
|
||||||
const auto z = InternalVersionT::zero;
|
const auto z = InternalVersionT::zero;
|
||||||
for (auto &v : childMaxVersion) {
|
for (auto &v : childMaxVersion) {
|
||||||
v = z;
|
v = z;
|
||||||
@@ -600,9 +555,6 @@ inline void Node48::copyChildrenAndKeyFrom(const Node16 &other) {
|
|||||||
std::max(maxOfMax[i >> Node48::kMaxOfMaxShift], childMaxVersion[i]);
|
std::max(maxOfMax[i >> Node48::kMaxOfMaxShift], childMaxVersion[i]);
|
||||||
++i;
|
++i;
|
||||||
}
|
}
|
||||||
// The reverseIndex bytes beyond numChildren are initialized so that the
|
|
||||||
// SIMD loads of a full 16-byte page in scan16 are well-defined (reading
|
|
||||||
// indeterminate bytes is UB in C++).
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void Node48::copyChildrenAndKeyFrom(const Node48 &other) {
|
inline void Node48::copyChildrenAndKeyFrom(const Node48 &other) {
|
||||||
@@ -629,7 +581,6 @@ inline void Node48::copyChildrenAndKeyFrom(const Node256 &other) {
|
|||||||
copyCommon(*this, other);
|
copyCommon(*this, other);
|
||||||
memset(index, -1, sizeof(index));
|
memset(index, -1, sizeof(index));
|
||||||
memset(children, 0, sizeof(children));
|
memset(children, 0, sizeof(children));
|
||||||
memset(reverseIndex, 0, sizeof(reverseIndex));
|
|
||||||
const auto z = InternalVersionT::zero;
|
const auto z = InternalVersionT::zero;
|
||||||
for (auto &v : childMaxVersion) {
|
for (auto &v : childMaxVersion) {
|
||||||
v = z;
|
v = z;
|
||||||
@@ -651,9 +602,6 @@ inline void Node48::copyChildrenAndKeyFrom(const Node256 &other) {
|
|||||||
++i;
|
++i;
|
||||||
});
|
});
|
||||||
memcpy(partialKey(), &other + 1, partialKeyLen);
|
memcpy(partialKey(), &other + 1, partialKeyLen);
|
||||||
// The reverseIndex bytes beyond numChildren are initialized so that the
|
|
||||||
// SIMD loads of a full 16-byte page in scan16 are well-defined (reading
|
|
||||||
// indeterminate bytes is UB in C++).
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void Node256::copyChildrenAndKeyFrom(const Node48 &other) {
|
inline void Node256::copyChildrenAndKeyFrom(const Node48 &other) {
|
||||||
@@ -963,34 +911,11 @@ int getNodeIndexExists(Node3 *self, uint8_t index) {
|
|||||||
|
|
||||||
int getNodeIndex(Node16 *self, uint8_t index) {
|
int getNodeIndex(Node16 *self, uint8_t index) {
|
||||||
|
|
||||||
#ifdef HAS_AVX
|
#if defined(__x86_64__) && !defined(USE_SIMD_FALLBACK)
|
||||||
// Based on https://www.the-paper-trail.org/post/art-paper-notes/
|
uint32_t bitfield =
|
||||||
|
find_eq_16(self->index, index) & ((1 << self->numChildren) - 1);
|
||||||
// key_vec is 16 repeated copies of the searched-for byte, one for every
|
|
||||||
// possible position in child_keys that needs to be searched.
|
|
||||||
__m128i key_vec = _mm_set1_epi8(index);
|
|
||||||
|
|
||||||
// Compare all child_keys to 'index' in parallel. Don't worry if some of the
|
|
||||||
// keys aren't valid, we'll mask the results to only consider the valid ones
|
|
||||||
// below.
|
|
||||||
__m128i indices;
|
|
||||||
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
|
|
||||||
// comparison (because the other values are meaningless)
|
|
||||||
uint32_t mask = (1 << self->numChildren) - 1;
|
|
||||||
|
|
||||||
// Change the results of the comparison into a bitfield, masking off any
|
|
||||||
// invalid comparisons.
|
|
||||||
uint32_t bitfield = _mm_movemask_epi8(results) & mask;
|
|
||||||
|
|
||||||
// No match if there are no '1's in the bitfield.
|
|
||||||
if (bitfield == 0)
|
if (bitfield == 0)
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
// Find the index of the first '1' in the bitfield by counting the leading
|
|
||||||
// zeros.
|
|
||||||
return std::countr_zero(bitfield);
|
return std::countr_zero(bitfield);
|
||||||
#elif defined(HAS_ARM_NEON)
|
#elif defined(HAS_ARM_NEON)
|
||||||
// Based on
|
// Based on
|
||||||
@@ -1023,13 +948,9 @@ int getNodeIndex(Node16 *self, uint8_t index) {
|
|||||||
|
|
||||||
int getNodeIndexExists(Node16 *self, uint8_t index) {
|
int getNodeIndexExists(Node16 *self, uint8_t index) {
|
||||||
|
|
||||||
#ifdef HAS_AVX
|
#if defined(__x86_64__) && !defined(USE_SIMD_FALLBACK)
|
||||||
__m128i key_vec = _mm_set1_epi8(index);
|
uint32_t bitfield =
|
||||||
__m128i indices;
|
find_eq_16(self->index, index) & ((1 << self->numChildren) - 1);
|
||||||
memcpy(&indices, self->index, Node16::kMaxNodes);
|
|
||||||
__m128i results = _mm_cmpeq_epi8(key_vec, indices);
|
|
||||||
uint32_t mask = (1 << self->numChildren) - 1;
|
|
||||||
uint32_t bitfield = _mm_movemask_epi8(results) & mask;
|
|
||||||
assume(bitfield != 0);
|
assume(bitfield != 0);
|
||||||
return std::countr_zero(bitfield);
|
return std::countr_zero(bitfield);
|
||||||
#elif defined(HAS_ARM_NEON)
|
#elif defined(HAS_ARM_NEON)
|
||||||
@@ -1319,13 +1240,9 @@ TaggedNodePointer getChildGeq(Node16 *self, int child) {
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef HAS_AVX
|
#if defined(__x86_64__) && !defined(USE_SIMD_FALLBACK)
|
||||||
__m128i key_vec = _mm_set1_epi8(child);
|
uint32_t bitfield =
|
||||||
__m128i indices;
|
find_ge_16(self->index, child) & ((1 << self->numChildren) - 1);
|
||||||
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;
|
|
||||||
return bitfield == 0 ? nullptr : self->children[std::countr_zero(bitfield)];
|
return bitfield == 0 ? nullptr : self->children[std::countr_zero(bitfield)];
|
||||||
#elif defined(HAS_ARM_NEON)
|
#elif defined(HAS_ARM_NEON)
|
||||||
uint8x16_t indices;
|
uint8x16_t indices;
|
||||||
@@ -2045,6 +1962,7 @@ Node *erase(Node *self, WriteContext *writeContext, bool logical) {
|
|||||||
parent48->index[parentIndex] = toRemoveChildrenIndex;
|
parent48->index[parentIndex] = toRemoveChildrenIndex;
|
||||||
parent48->reverseIndex[toRemoveChildrenIndex] = parentIndex;
|
parent48->reverseIndex[toRemoveChildrenIndex] = parentIndex;
|
||||||
}
|
}
|
||||||
|
parent48->childMaxVersion[lastChildrenIndex] = writeContext->zero;
|
||||||
VALGRIND_MAKE_MEM_UNDEFINED(parent48->children + lastChildrenIndex,
|
VALGRIND_MAKE_MEM_UNDEFINED(parent48->children + lastChildrenIndex,
|
||||||
sizeof(parent48->children[0]));
|
sizeof(parent48->children[0]));
|
||||||
|
|
||||||
@@ -2139,53 +2057,6 @@ compare16_avx512(const InternalVersionT *vs, InternalVersionT rv) {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef HAS_ARM_NEON
|
|
||||||
// Returns a uint8x16_t whose byte i is 0xff if vs[i] > readVersion and 0
|
|
||||||
// otherwise. When USE_64_BIT is set the comparison uses the full 64-bit value
|
|
||||||
// (via vcgtq_s64); otherwise it falls back to the 32-bit subtract-and-compare
|
|
||||||
// used by the original NEON fast path. The caller narrows this 16-byte vector
|
|
||||||
// into the nibble-packed (4 bits per version) mask expected by scan16 and
|
|
||||||
// checkMaxBetweenExclusiveImpl.
|
|
||||||
inline uint8x16_t conflictMask16(const InternalVersionT *vs,
|
|
||||||
InternalVersionT readVersion) {
|
|
||||||
#if USE_64_BIT
|
|
||||||
int64_t rv;
|
|
||||||
memcpy(&rv, &readVersion, sizeof(rv));
|
|
||||||
const auto rvVec = vdupq_n_s64(rv);
|
|
||||||
int32x2_t r32[8];
|
|
||||||
const auto *vsp = reinterpret_cast<const int64_t *>(vs);
|
|
||||||
for (int j = 0; j < 8; ++j) {
|
|
||||||
r32[j] = vmovn_s64(
|
|
||||||
vreinterpretq_s64_u64(vcgtq_s64(vld1q_s64(vsp + 2 * j), rvVec)));
|
|
||||||
}
|
|
||||||
uint32x4_t w4[4];
|
|
||||||
for (int k = 0; k < 4; ++k) {
|
|
||||||
w4[k] = vreinterpretq_u32_s32(vcombine_s32(r32[2 * k], r32[2 * k + 1]));
|
|
||||||
}
|
|
||||||
return vcombine_u8(
|
|
||||||
vmovn_u16(vcombine_u16(vmovn_u32(w4[0]), vmovn_u32(w4[1]))),
|
|
||||||
vmovn_u16(vcombine_u16(vmovn_u32(w4[2]), vmovn_u32(w4[3]))));
|
|
||||||
#else
|
|
||||||
uint32x4_t w4[4];
|
|
||||||
memcpy(w4, vs, sizeof(w4));
|
|
||||||
uint32_t rv;
|
|
||||||
memcpy(&rv, &readVersion, sizeof(rv));
|
|
||||||
const auto rvVec = vdupq_n_u32(rv);
|
|
||||||
|
|
||||||
int32x4_t z;
|
|
||||||
memset(&z, 0, sizeof(z));
|
|
||||||
|
|
||||||
uint16x4_t conflicting[4];
|
|
||||||
for (int i = 0; i < 4; ++i) {
|
|
||||||
conflicting[i] =
|
|
||||||
vmovn_u32(vcgtq_s32(vreinterpretq_s32_u32(vsubq_u32(w4[i], rvVec)), z));
|
|
||||||
}
|
|
||||||
return vcombine_u8(vmovn_u16(vcombine_u16(conflicting[0], conflicting[1])),
|
|
||||||
vmovn_u16(vcombine_u16(conflicting[2], conflicting[3])));
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Returns true if v[i] <= readVersion for all i such that begin <= is[i] < end
|
// Returns true if v[i] <= readVersion for all i such that begin <= is[i] < end
|
||||||
// Preconditions: begin <= end, end - begin < 256
|
// Preconditions: begin <= end, end - begin < 256
|
||||||
template <bool kAVX512>
|
template <bool kAVX512>
|
||||||
@@ -2206,20 +2077,32 @@ bool scan16(const InternalVersionT *vs, const uint8_t *is, int begin, int end,
|
|||||||
uint64_t mask = vget_lane_u64(
|
uint64_t mask = vget_lane_u64(
|
||||||
vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(results), 4)), 0);
|
vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(results), 4)), 0);
|
||||||
|
|
||||||
|
uint32x4_t w4[4];
|
||||||
|
memcpy(w4, vs, sizeof(w4));
|
||||||
|
uint32_t rv;
|
||||||
|
memcpy(&rv, &readVersion, sizeof(rv));
|
||||||
|
const auto rvVec = vdupq_n_u32(rv);
|
||||||
|
|
||||||
|
int32x4_t z;
|
||||||
|
memset(&z, 0, sizeof(z));
|
||||||
|
|
||||||
|
uint16x4_t conflicting[4];
|
||||||
|
for (int i = 0; i < 4; ++i) {
|
||||||
|
conflicting[i] =
|
||||||
|
vmovn_u32(vcgtq_s32(vreinterpretq_s32_u32(vsubq_u32(w4[i], rvVec)), z));
|
||||||
|
}
|
||||||
|
auto combined =
|
||||||
|
vcombine_u8(vmovn_u16(vcombine_u16(conflicting[0], conflicting[1])),
|
||||||
|
vmovn_u16(vcombine_u16(conflicting[2], conflicting[3])));
|
||||||
|
|
||||||
uint64_t compared = vget_lane_u64(
|
uint64_t compared = vget_lane_u64(
|
||||||
vreinterpret_u64_u8(vshrn_n_u16(
|
vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(combined), 4)), 0);
|
||||||
vreinterpretq_u16_u8(conflictMask16(vs, readVersion)), 4)),
|
|
||||||
0);
|
|
||||||
|
|
||||||
return !(compared & mask);
|
return !(compared & mask);
|
||||||
|
|
||||||
#elif defined(HAS_AVX)
|
#elif defined(__x86_64__) && !defined(USE_SIMD_FALLBACK)
|
||||||
|
|
||||||
__m128i indices;
|
uint32_t mask = mask_in_range_16(is, begin, end);
|
||||||
memcpy(&indices, is, 16);
|
|
||||||
indices = _mm_sub_epi8(indices, _mm_set1_epi8(begin));
|
|
||||||
uint32_t mask = ~_mm_movemask_epi8(_mm_cmpeq_epi8(
|
|
||||||
indices, _mm_max_epu8(indices, _mm_set1_epi8(end - begin))));
|
|
||||||
|
|
||||||
uint32_t compared = 0;
|
uint32_t compared = 0;
|
||||||
if constexpr (kAVX512) {
|
if constexpr (kAVX512) {
|
||||||
@@ -2259,10 +2142,26 @@ bool scan16(const InternalVersionT *vs, int begin, int end,
|
|||||||
assert(begin <= end);
|
assert(begin <= end);
|
||||||
|
|
||||||
#if defined(HAS_ARM_NEON)
|
#if defined(HAS_ARM_NEON)
|
||||||
|
uint32x4_t w4[4];
|
||||||
|
memcpy(w4, vs, sizeof(w4));
|
||||||
|
uint32_t rv;
|
||||||
|
memcpy(&rv, &readVersion, sizeof(rv));
|
||||||
|
const auto rvVec = vdupq_n_u32(rv);
|
||||||
|
|
||||||
|
int32x4_t z;
|
||||||
|
memset(&z, 0, sizeof(z));
|
||||||
|
|
||||||
|
uint16x4_t conflicting[4];
|
||||||
|
for (int i = 0; i < 4; ++i) {
|
||||||
|
conflicting[i] =
|
||||||
|
vmovn_u32(vcgtq_s32(vreinterpretq_s32_u32(vsubq_u32(w4[i], rvVec)), z));
|
||||||
|
}
|
||||||
|
auto combined =
|
||||||
|
vcombine_u8(vmovn_u16(vcombine_u16(conflicting[0], conflicting[1])),
|
||||||
|
vmovn_u16(vcombine_u16(conflicting[2], conflicting[3])));
|
||||||
|
|
||||||
uint64_t conflict = vget_lane_u64(
|
uint64_t conflict = vget_lane_u64(
|
||||||
vreinterpret_u64_u8(vshrn_n_u16(
|
vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(combined), 4)), 0);
|
||||||
vreinterpretq_u16_u8(conflictMask16(vs, readVersion)), 4)),
|
|
||||||
0);
|
|
||||||
|
|
||||||
conflict &= end == 16 ? -1 : (uint64_t(1) << (end << 2)) - 1;
|
conflict &= end == 16 ? -1 : (uint64_t(1) << (end << 2)) - 1;
|
||||||
conflict >>= begin << 2;
|
conflict >>= begin << 2;
|
||||||
@@ -2311,8 +2210,6 @@ bool checkMaxBetweenExclusiveImpl(Node3 *n, int begin, int end,
|
|||||||
assert(!(begin == -1 && end == 256));
|
assert(!(begin == -1 && end == 256));
|
||||||
|
|
||||||
auto *self = static_cast<Node3 *>(n);
|
auto *self = static_cast<Node3 *>(n);
|
||||||
assume(1 <= self->numChildren);
|
|
||||||
assume(self->numChildren <= 3);
|
|
||||||
|
|
||||||
++begin;
|
++begin;
|
||||||
|
|
||||||
@@ -2378,23 +2275,33 @@ bool checkMaxBetweenExclusiveImpl(Node16 *n, int begin, int end,
|
|||||||
const bool firstRangeOk =
|
const bool firstRangeOk =
|
||||||
!child->entryPresent || child->entry.rangeVersion <= readVersion;
|
!child->entryPresent || child->entry.rangeVersion <= readVersion;
|
||||||
|
|
||||||
|
uint32x4_t w4[4];
|
||||||
|
memcpy(w4, self->childMaxVersion, sizeof(w4));
|
||||||
|
uint32_t rv;
|
||||||
|
memcpy(&rv, &readVersion, sizeof(rv));
|
||||||
|
const auto rvVec = vdupq_n_u32(rv);
|
||||||
|
|
||||||
|
int32x4_t z;
|
||||||
|
memset(&z, 0, sizeof(z));
|
||||||
|
|
||||||
|
uint16x4_t conflicting[4];
|
||||||
|
for (int i = 0; i < 4; ++i) {
|
||||||
|
conflicting[i] =
|
||||||
|
vmovn_u32(vcgtq_s32(vreinterpretq_s32_u32(vsubq_u32(w4[i], rvVec)), z));
|
||||||
|
}
|
||||||
|
auto combined =
|
||||||
|
vcombine_u8(vmovn_u16(vcombine_u16(conflicting[0], conflicting[1])),
|
||||||
|
vmovn_u16(vcombine_u16(conflicting[2], conflicting[3])));
|
||||||
|
|
||||||
uint64_t compared = vget_lane_u64(
|
uint64_t compared = vget_lane_u64(
|
||||||
vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(conflictMask16(
|
vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(combined), 4)), 0);
|
||||||
self->childMaxVersion, readVersion)),
|
|
||||||
4)),
|
|
||||||
0);
|
|
||||||
|
|
||||||
return !(compared & mask) && firstRangeOk;
|
return !(compared & mask) && firstRangeOk;
|
||||||
|
|
||||||
#elif defined(HAS_AVX)
|
#elif defined(__x86_64__) && !defined(USE_SIMD_FALLBACK)
|
||||||
|
|
||||||
__m128i indices;
|
uint32_t mask = mask_in_range_16(self->index, begin, end) &
|
||||||
memcpy(&indices, self->index, 16);
|
((1 << self->numChildren) - 1);
|
||||||
indices = _mm_sub_epi8(indices, _mm_set1_epi8(begin));
|
|
||||||
uint32_t mask =
|
|
||||||
0xffff & ~_mm_movemask_epi8(_mm_cmpeq_epi8(
|
|
||||||
indices, _mm_max_epu8(indices, _mm_set1_epi8(end - begin))));
|
|
||||||
mask &= (1 << self->numChildren) - 1;
|
|
||||||
if (!mask) {
|
if (!mask) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -2786,39 +2693,10 @@ checkMaxBetweenExclusiveImpl<true>(Node256 *n, int begin, int end,
|
|||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
#if SHOW_MEMORY
|
|
||||||
// Length of the key leading to `child`, given the length of the key leading to
|
|
||||||
// its parent. Consistent with `getSearchPathLength`.
|
|
||||||
int64_t childSearchPathLength(int64_t parentSearchPathLength, Node *child) {
|
|
||||||
return parentSearchPathLength + 1 + child->partialKeyLen;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
void eraseTree(Node *root, WriteContext *writeContext) {
|
void eraseTree(Node *root, WriteContext *writeContext) {
|
||||||
Arena arena;
|
Arena arena;
|
||||||
auto toFree = vector<Node *>(arena);
|
auto toFree = vector<Node *>(arena);
|
||||||
toFree.push_back(root);
|
toFree.push_back(root);
|
||||||
// Length of the key leading to each node in `toFree`, on a parallel stack.
|
|
||||||
// A node is released before its children are visited, so the `parent`
|
|
||||||
// pointers that `getSearchPathLength` walks are dangling by the time a queued
|
|
||||||
// node is popped. Carry the lengths along instead. Only the SHOW_MEMORY
|
|
||||||
// accounting needs them.
|
|
||||||
#if SHOW_MEMORY
|
|
||||||
auto searchPathLengths = vector<int64_t>(arena);
|
|
||||||
searchPathLengths.push_back(getSearchPathLength(root));
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Queue `child` for release, where `parentSearchPathLength` is the length of
|
|
||||||
// the key leading to its parent.
|
|
||||||
auto queueChild = [&](Node *child, int64_t parentSearchPathLength) {
|
|
||||||
toFree.push_back(child);
|
|
||||||
#if SHOW_MEMORY
|
|
||||||
searchPathLengths.push_back(
|
|
||||||
childSearchPathLength(parentSearchPathLength, child));
|
|
||||||
#else
|
|
||||||
(void)parentSearchPathLength;
|
|
||||||
#endif
|
|
||||||
};
|
|
||||||
|
|
||||||
while (toFree.size() > 0) {
|
while (toFree.size() > 0) {
|
||||||
auto *n = toFree.back();
|
auto *n = toFree.back();
|
||||||
@@ -2826,13 +2704,7 @@ void eraseTree(Node *root, WriteContext *writeContext) {
|
|||||||
writeContext->accum.entries_erased += n->entryPresent;
|
writeContext->accum.entries_erased += n->entryPresent;
|
||||||
++writeContext->accum.nodes_released;
|
++writeContext->accum.nodes_released;
|
||||||
|
|
||||||
#if SHOW_MEMORY
|
removeKey(n);
|
||||||
const int64_t searchPathLength = searchPathLengths.back();
|
|
||||||
searchPathLengths.pop_back();
|
|
||||||
#else
|
|
||||||
const int64_t searchPathLength = 0;
|
|
||||||
#endif
|
|
||||||
removeKey(n, searchPathLength);
|
|
||||||
|
|
||||||
switch (n->getType()) {
|
switch (n->getType()) {
|
||||||
case Type_Node0: {
|
case Type_Node0: {
|
||||||
@@ -2842,39 +2714,28 @@ void eraseTree(Node *root, WriteContext *writeContext) {
|
|||||||
case Type_Node3: {
|
case Type_Node3: {
|
||||||
auto *n3 = static_cast<Node3 *>(n);
|
auto *n3 = static_cast<Node3 *>(n);
|
||||||
for (int i = 0; i < n3->numChildren; ++i) {
|
for (int i = 0; i < n3->numChildren; ++i) {
|
||||||
queueChild(n3->children[i], searchPathLength);
|
toFree.push_back(n3->children[i]);
|
||||||
}
|
}
|
||||||
writeContext->release(n3);
|
writeContext->release(n3);
|
||||||
} break;
|
} break;
|
||||||
case Type_Node16: {
|
case Type_Node16: {
|
||||||
auto *n16 = static_cast<Node16 *>(n);
|
auto *n16 = static_cast<Node16 *>(n);
|
||||||
for (int i = 0; i < n16->numChildren; ++i) {
|
for (int i = 0; i < n16->numChildren; ++i) {
|
||||||
queueChild(n16->children[i], searchPathLength);
|
toFree.push_back(n16->children[i]);
|
||||||
}
|
}
|
||||||
writeContext->release(n16);
|
writeContext->release(n16);
|
||||||
} break;
|
} break;
|
||||||
case Type_Node48: {
|
case Type_Node48: {
|
||||||
auto *n48 = static_cast<Node48 *>(n);
|
auto *n48 = static_cast<Node48 *>(n);
|
||||||
for (int i = 0; i < n48->numChildren; ++i) {
|
for (int i = 0; i < n48->numChildren; ++i) {
|
||||||
queueChild(n48->children[i], searchPathLength);
|
toFree.push_back(n48->children[i]);
|
||||||
}
|
}
|
||||||
writeContext->release(n48);
|
writeContext->release(n48);
|
||||||
} break;
|
} break;
|
||||||
case Type_Node256: {
|
case Type_Node256: {
|
||||||
auto *n256 = static_cast<Node256 *>(n);
|
auto *n256 = static_cast<Node256 *>(n);
|
||||||
auto *out = toFree.unsafePrepareAppend(n256->numChildren).data();
|
auto *out = toFree.unsafePrepareAppend(n256->numChildren).data();
|
||||||
#if SHOW_MEMORY
|
n256->bitSet.forEachSet([&](int i) { *out++ = n256->children[i]; });
|
||||||
auto *outSearchPathLengths =
|
|
||||||
searchPathLengths.unsafePrepareAppend(n256->numChildren).data();
|
|
||||||
#endif
|
|
||||||
n256->bitSet.forEachSet([&](int i) {
|
|
||||||
Node *child = n256->children[i];
|
|
||||||
*out++ = child;
|
|
||||||
#if SHOW_MEMORY
|
|
||||||
*outSearchPathLengths++ =
|
|
||||||
childSearchPathLength(searchPathLength, child);
|
|
||||||
#endif
|
|
||||||
});
|
|
||||||
assert(out == toFree.end());
|
assert(out == toFree.end());
|
||||||
writeContext->release(n256);
|
writeContext->release(n256);
|
||||||
} break;
|
} break;
|
||||||
@@ -2971,11 +2832,7 @@ horizontalMax16(InternalVersionT *vs, InternalVersionT) {
|
|||||||
return max;
|
return max;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @pre `node` is not the root, `node->entryPresent`, and `writeContext !=
|
// Precondition: `node->entryPresent`, and node is not the root
|
||||||
* nullptr`.
|
|
||||||
* @post `maxVersion(node)` is the max of `writeContext->zero` and the max
|
|
||||||
* version of all logical keys starting with the search path of `node`.
|
|
||||||
*/
|
|
||||||
void fixupMaxVersion(Node *node, WriteContext *writeContext) {
|
void fixupMaxVersion(Node *node, WriteContext *writeContext) {
|
||||||
assert(node->parent);
|
assert(node->parent);
|
||||||
InternalVersionT max;
|
InternalVersionT max;
|
||||||
@@ -2986,13 +2843,9 @@ void fixupMaxVersion(Node *node, WriteContext *writeContext) {
|
|||||||
break;
|
break;
|
||||||
case Type_Node3: {
|
case Type_Node3: {
|
||||||
auto *self3 = static_cast<Node3 *>(node);
|
auto *self3 = static_cast<Node3 *>(node);
|
||||||
assume(1 <= self3->numChildren);
|
max = std::max(max,
|
||||||
assume(self3->numChildren <= 3);
|
horizontalMaxUpTo16(self3->childMaxVersion,
|
||||||
InternalVersionT m = self3->childMaxVersion[0];
|
writeContext->zero, self3->numChildren));
|
||||||
for (int i = 1; i < self3->numChildren; ++i) {
|
|
||||||
m = std::max(m, self3->childMaxVersion[i]);
|
|
||||||
}
|
|
||||||
max = std::max(max, m);
|
|
||||||
} break;
|
} break;
|
||||||
case Type_Node16: {
|
case Type_Node16: {
|
||||||
auto *self16 = static_cast<Node16 *>(node);
|
auto *self16 = static_cast<Node16 *>(node);
|
||||||
@@ -3008,7 +2861,8 @@ void fixupMaxVersion(Node *node, WriteContext *writeContext) {
|
|||||||
} break;
|
} break;
|
||||||
case Type_Node256: {
|
case Type_Node256: {
|
||||||
auto *self256 = static_cast<Node256 *>(node);
|
auto *self256 = static_cast<Node256 *>(node);
|
||||||
max = std::max(max, horizontalMax16(self256->maxOfMax, writeContext->zero));
|
max = std::max(
|
||||||
|
max, horizontalMax16(self256->childMaxVersion, writeContext->zero));
|
||||||
} break;
|
} break;
|
||||||
default: // GCOVR_EXCL_LINE
|
default: // GCOVR_EXCL_LINE
|
||||||
__builtin_unreachable(); // GCOVR_EXCL_LINE
|
__builtin_unreachable(); // GCOVR_EXCL_LINE
|
||||||
@@ -4955,7 +4809,7 @@ struct __attribute__((visibility("hidden"))) ConflictSet::Impl {
|
|||||||
auto *results2 = new (arena) Result[count];
|
auto *results2 = new (arena) Result[count];
|
||||||
check::Context context2;
|
check::Context context2;
|
||||||
useSequential(reads, results2, count, context2);
|
useSequential(reads, results2, count, context2);
|
||||||
assert(memcmp(result, results2, count * sizeof(Result)) == 0);
|
assert(memcmp(result, results2, count) == 0);
|
||||||
assert(context.readContext == context2.readContext);
|
assert(context.readContext == context2.readContext);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@@ -5056,17 +4910,15 @@ struct __attribute__((visibility("hidden"))) ConflictSet::Impl {
|
|||||||
}
|
}
|
||||||
if (e != nullptr) {
|
if (e != nullptr) {
|
||||||
while (e->releaseDeferred) {
|
while (e->releaseDeferred) {
|
||||||
e = e->forwardTo;
|
e = b->forwardTo;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fprintf(stderr, "search path: %s, begin: %s\n",
|
fprintf(stderr, "search path: %s, begin: %s\n",
|
||||||
getSearchPathPrintable(b).c_str(),
|
getSearchPathPrintable(b).c_str(),
|
||||||
printable(context.results[i].remaining).c_str());
|
printable(context.results[i].remaining).c_str());
|
||||||
if (e != nullptr) {
|
fprintf(stderr, "search path: %s, end: %s\n",
|
||||||
fprintf(stderr, "search path: %s, end: %s\n",
|
getSearchPathPrintable(e).c_str(),
|
||||||
getSearchPathPrintable(e).c_str(),
|
printable(context.results[i].endRemaining).c_str());
|
||||||
printable(context.results[i].endRemaining).c_str());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -5168,7 +5020,7 @@ struct __attribute__((visibility("hidden"))) ConflictSet::Impl {
|
|||||||
|
|
||||||
if (oldestExtantVersion < writeVersion - kMaxCorrectVersionWindow)
|
if (oldestExtantVersion < writeVersion - kMaxCorrectVersionWindow)
|
||||||
[[unlikely]] {
|
[[unlikely]] {
|
||||||
if (writeVersion - kNominalVersionWindow > newestVersionFullPrecision) {
|
if (writeVersion > newestVersionFullPrecision + kNominalVersionWindow) {
|
||||||
eraseTree(rootParent->children[0], &writeContext);
|
eraseTree(rootParent->children[0], &writeContext);
|
||||||
init(writeVersion - kNominalVersionWindow);
|
init(writeVersion - kNominalVersionWindow);
|
||||||
}
|
}
|
||||||
@@ -5546,9 +5398,8 @@ void internal_addWrites(ConflictSet::Impl *impl,
|
|||||||
impl->totalBytes += mallocBytesDelta;
|
impl->totalBytes += mallocBytesDelta;
|
||||||
impl->memory_bytes.set(impl->totalBytes);
|
impl->memory_bytes.set(impl->totalBytes);
|
||||||
#if SHOW_MEMORY
|
#if SHOW_MEMORY
|
||||||
// Testing-only cross-check of the accounting.
|
|
||||||
if (impl->totalBytes != mallocBytes) {
|
if (impl->totalBytes != mallocBytes) {
|
||||||
abort(); // GCOVR_EXCL_LINE
|
abort();
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@@ -5559,9 +5410,8 @@ void internal_setOldestVersion(ConflictSet::Impl *impl, int64_t oldestVersion) {
|
|||||||
impl->totalBytes += mallocBytesDelta;
|
impl->totalBytes += mallocBytesDelta;
|
||||||
impl->memory_bytes.set(impl->totalBytes);
|
impl->memory_bytes.set(impl->totalBytes);
|
||||||
#if SHOW_MEMORY
|
#if SHOW_MEMORY
|
||||||
// Testing-only cross-check of the accounting.
|
|
||||||
if (impl->totalBytes != mallocBytes) {
|
if (impl->totalBytes != mallocBytes) {
|
||||||
abort(); // GCOVR_EXCL_LINE
|
abort();
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@@ -6027,14 +5877,6 @@ checkMaxVersion(Node *root, Node *node, InternalVersionT oldestVersion,
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
// The SHOW_MEMORY instrumentation. Each of these functions is defined exactly
|
|
||||||
// once, with the accounting compiled out when SHOW_MEMORY is disabled. Defining
|
|
||||||
// them once (rather than defining no-op stubs when SHOW_MEMORY is disabled)
|
|
||||||
// keeps every configuration of this file reporting each function on the same
|
|
||||||
// line, which merging coverage from objects compiled with different options
|
|
||||||
// requires: gcovr, merging the SHOW_MEMORY test with the rest of the build,
|
|
||||||
// otherwise aborts with "Got function ... on multiple lines". The optimizer
|
|
||||||
// removes the resulting empty calls.
|
|
||||||
#if SHOW_MEMORY
|
#if SHOW_MEMORY
|
||||||
|
|
||||||
int64_t nodeBytes = 0;
|
int64_t nodeBytes = 0;
|
||||||
@@ -6063,10 +5905,7 @@ int64_t getNodeSize(struct Node *n) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
||||||
int64_t getSearchPathLength(Node *n) {
|
int64_t getSearchPathLength(Node *n) {
|
||||||
#if SHOW_MEMORY
|
|
||||||
assert(n != nullptr);
|
assert(n != nullptr);
|
||||||
int64_t result = 0;
|
int64_t result = 0;
|
||||||
for (;;) {
|
for (;;) {
|
||||||
@@ -6078,14 +5917,9 @@ int64_t getSearchPathLength(Node *n) {
|
|||||||
n = n->parent;
|
n = n->parent;
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
#else
|
|
||||||
(void)n;
|
|
||||||
return 0;
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void addNode(Node *n) {
|
void addNode(Node *n) {
|
||||||
#if SHOW_MEMORY
|
|
||||||
nodeBytes += getNodeSize(n);
|
nodeBytes += getNodeSize(n);
|
||||||
partialCapacityBytes += n->getCapacity();
|
partialCapacityBytes += n->getCapacity();
|
||||||
if (nodeBytes > peakNodeBytes) {
|
if (nodeBytes > peakNodeBytes) {
|
||||||
@@ -6094,22 +5928,14 @@ void addNode(Node *n) {
|
|||||||
if (partialCapacityBytes > peakPartialCapacityBytes) {
|
if (partialCapacityBytes > peakPartialCapacityBytes) {
|
||||||
peakPartialCapacityBytes = partialCapacityBytes;
|
peakPartialCapacityBytes = partialCapacityBytes;
|
||||||
}
|
}
|
||||||
#else
|
|
||||||
(void)n;
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeNode(Node *n) {
|
void removeNode(Node *n) {
|
||||||
#if SHOW_MEMORY
|
|
||||||
nodeBytes -= getNodeSize(n);
|
nodeBytes -= getNodeSize(n);
|
||||||
partialCapacityBytes -= n->getCapacity();
|
partialCapacityBytes -= n->getCapacity();
|
||||||
#else
|
|
||||||
(void)n;
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void addKey(Node *n) {
|
void addKey(Node *n) {
|
||||||
#if SHOW_MEMORY
|
|
||||||
if (!n->entryPresent) {
|
if (!n->entryPresent) {
|
||||||
++totalKeys;
|
++totalKeys;
|
||||||
keyBytes += getSearchPathLength(n);
|
keyBytes += getSearchPathLength(n);
|
||||||
@@ -6120,36 +5946,15 @@ void addKey(Node *n) {
|
|||||||
peakKeyBytes = keyBytes;
|
peakKeyBytes = keyBytes;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#else
|
|
||||||
(void)n;
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeKey(Node *n) {
|
void removeKey(Node *n) {
|
||||||
#if SHOW_MEMORY
|
|
||||||
if (n->entryPresent) {
|
if (n->entryPresent) {
|
||||||
--totalKeys;
|
--totalKeys;
|
||||||
keyBytes -= getSearchPathLength(n);
|
keyBytes -= getSearchPathLength(n);
|
||||||
}
|
}
|
||||||
#else
|
|
||||||
(void)n;
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeKey(Node *n, int64_t searchPathLength) {
|
|
||||||
#if SHOW_MEMORY
|
|
||||||
if (n->entryPresent) {
|
|
||||||
--totalKeys;
|
|
||||||
keyBytes -= searchPathLength;
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
(void)n;
|
|
||||||
(void)searchPathLength;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
#if SHOW_MEMORY
|
|
||||||
|
|
||||||
struct __attribute__((visibility("default"))) PeakPrinter {
|
struct __attribute__((visibility("default"))) PeakPrinter {
|
||||||
~PeakPrinter() {
|
~PeakPrinter() {
|
||||||
printf("--- radix_tree ---\n");
|
printf("--- radix_tree ---\n");
|
||||||
|
|||||||
@@ -59,12 +59,6 @@ struct __attribute__((visibility("hidden"))) ConflictSet::Impl {
|
|||||||
while (keyUpdates > 0) {
|
while (keyUpdates > 0) {
|
||||||
if (iter == map.end()) {
|
if (iter == map.end()) {
|
||||||
iter = map.begin();
|
iter = map.begin();
|
||||||
// The map is empty, so there are no more entries to garbage-collect.
|
|
||||||
// Break rather than spinning forever (keyUpdates is charged at 2x
|
|
||||||
// the entry count, so it may still be > 0 after emptying the map).
|
|
||||||
if (iter == map.end()) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for (; iter != map.end(); --keyUpdates) {
|
for (; iter != map.end(); --keyUpdates) {
|
||||||
if (iter->second <= oldestVersion) {
|
if (iter->second <= oldestVersion) {
|
||||||
|
|||||||
@@ -23,9 +23,7 @@ using namespace weaselab;
|
|||||||
#include <callgrind.h>
|
#include <callgrind.h>
|
||||||
|
|
||||||
#define DEBUG_VERBOSE 0
|
#define DEBUG_VERBOSE 0
|
||||||
#ifndef SHOW_MEMORY
|
|
||||||
#define SHOW_MEMORY 0
|
#define SHOW_MEMORY 0
|
||||||
#endif
|
|
||||||
|
|
||||||
// std::span is not trivially constructible. We want a span that leaves its
|
// std::span is not trivially constructible. We want a span that leaves its
|
||||||
// members uninitialized for performance reasons.
|
// members uninitialized for performance reasons.
|
||||||
@@ -98,11 +96,6 @@ static_assert(std::is_trivial_v<TrivialSpan>);
|
|||||||
#if SHOW_MEMORY
|
#if SHOW_MEMORY
|
||||||
inline int64_t mallocBytes = 0;
|
inline int64_t mallocBytes = 0;
|
||||||
inline int64_t peakMallocBytes = 0;
|
inline int64_t peakMallocBytes = 0;
|
||||||
// Defined in ConflictSet.cpp
|
|
||||||
extern int64_t nodeBytes;
|
|
||||||
extern int64_t partialCapacityBytes;
|
|
||||||
extern int64_t totalKeys;
|
|
||||||
extern int64_t keyBytes;
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
inline thread_local int64_t mallocBytesDelta = 0;
|
inline thread_local int64_t mallocBytesDelta = 0;
|
||||||
|
|||||||
+2
-70
@@ -96,7 +96,7 @@ inline int firstNeqStride(const uint8_t *ap, const uint8_t *bp) {
|
|||||||
// GCOVR_EXCL_START
|
// GCOVR_EXCL_START
|
||||||
#if defined(HAS_AVX) && !defined(__SANITIZE_THREAD__)
|
#if defined(HAS_AVX) && !defined(__SANITIZE_THREAD__)
|
||||||
__attribute__((target("avx512f,avx512bw"))) inline int
|
__attribute__((target("avx512f,avx512bw"))) inline int
|
||||||
longestCommonPrefixLong(const uint8_t *ap, const uint8_t *bp, int cl) {
|
longestCommonPrefix(const uint8_t *ap, const uint8_t *bp, int cl) {
|
||||||
int i = 0;
|
int i = 0;
|
||||||
int end = cl & ~63;
|
int end = cl & ~63;
|
||||||
while (i < end) {
|
while (i < end) {
|
||||||
@@ -123,7 +123,7 @@ __attribute__((target("default")))
|
|||||||
// GCOVR_EXCL_STOP
|
// GCOVR_EXCL_STOP
|
||||||
|
|
||||||
inline int
|
inline int
|
||||||
longestCommonPrefixLong(const uint8_t *ap, const uint8_t *bp, int cl) {
|
longestCommonPrefix(const uint8_t *ap, const uint8_t *bp, int cl) {
|
||||||
if (!(cl >= 0)) {
|
if (!(cl >= 0)) {
|
||||||
__builtin_unreachable(); // GCOVR_EXCL_LINE
|
__builtin_unreachable(); // GCOVR_EXCL_LINE
|
||||||
}
|
}
|
||||||
@@ -183,71 +183,3 @@ longestCommonPrefixLong(const uint8_t *ap, const uint8_t *bp, int cl) {
|
|||||||
|
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
// Use in-bounds native-word loads for short common lengths. The second load
|
|
||||||
// overlaps the first; because the first load matched, any mismatch found in
|
|
||||||
// the overlap is impossible and the returned absolute index is still correct.
|
|
||||||
// Precondition: cl <= 2 * sizeof(uint64_t)
|
|
||||||
__attribute__((always_inline)) static inline int
|
|
||||||
longestCommonPrefixShort(const uint8_t *ap, const uint8_t *bp, int cl) {
|
|
||||||
assert(cl <= 2 * int(sizeof(uint64_t)));
|
|
||||||
if (cl >= 8) {
|
|
||||||
uint64_t a0;
|
|
||||||
uint64_t b0;
|
|
||||||
memcpy(&a0, ap, sizeof(a0));
|
|
||||||
memcpy(&b0, bp, sizeof(b0));
|
|
||||||
if (uint64_t mismatched = a0 ^ b0) {
|
|
||||||
return std::countr_zero(mismatched) / 8;
|
|
||||||
}
|
|
||||||
if (cl == 8) {
|
|
||||||
return 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint64_t a1;
|
|
||||||
uint64_t b1;
|
|
||||||
memcpy(&a1, ap + cl - 8, sizeof(a1));
|
|
||||||
memcpy(&b1, bp + cl - 8, sizeof(b1));
|
|
||||||
if (uint64_t mismatched = a1 ^ b1) {
|
|
||||||
return cl - 8 + std::countr_zero(mismatched) / 8;
|
|
||||||
}
|
|
||||||
return cl;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cl >= 4) {
|
|
||||||
uint32_t a0;
|
|
||||||
uint32_t b0;
|
|
||||||
memcpy(&a0, ap, sizeof(a0));
|
|
||||||
memcpy(&b0, bp, sizeof(b0));
|
|
||||||
if (uint32_t mismatched = a0 ^ b0) {
|
|
||||||
return std::countr_zero(mismatched) / 8;
|
|
||||||
}
|
|
||||||
if (cl == 4) {
|
|
||||||
return 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t a1;
|
|
||||||
uint32_t b1;
|
|
||||||
memcpy(&a1, ap + cl - 4, sizeof(a1));
|
|
||||||
memcpy(&b1, bp + cl - 4, sizeof(b1));
|
|
||||||
if (uint32_t mismatched = a1 ^ b1) {
|
|
||||||
return cl - 4 + std::countr_zero(mismatched) / 8;
|
|
||||||
}
|
|
||||||
return cl;
|
|
||||||
}
|
|
||||||
|
|
||||||
int i = 0;
|
|
||||||
while (i < cl && ap[i] == bp[i]) {
|
|
||||||
++i;
|
|
||||||
}
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
|
|
||||||
__attribute__((always_inline)) static inline int
|
|
||||||
longestCommonPrefix(const uint8_t *ap, const uint8_t *bp, int cl) {
|
|
||||||
if (!(cl >= 0)) {
|
|
||||||
__builtin_unreachable(); // GCOVR_EXCL_LINE
|
|
||||||
}
|
|
||||||
if (cl <= 2 * int(sizeof(uint64_t))) {
|
|
||||||
return longestCommonPrefixShort(ap, bp, cl);
|
|
||||||
}
|
|
||||||
return longestCommonPrefixLong(ap, bp, cl);
|
|
||||||
}
|
|
||||||
@@ -19,30 +19,30 @@ InstalledDir: /usr/lib/llvm-21/bin
|
|||||||
|
|
||||||
| ns/op | op/s | err% | ins/op | cyc/op | IPC | bra/op | miss% | total | benchmark
|
| ns/op | op/s | err% | ins/op | cyc/op | IPC | bra/op | miss% | total | benchmark
|
||||||
|--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:----------
|
|--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:----------
|
||||||
| 165.93 | 6,026,520.04 | 0.1% | 3,107.03 | 610.42 | 5.090 | 558.59 | 0.0% | 1.98 | `point reads`
|
| 164.29 | 6,086,873.38 | 0.0% | 3,107.03 | 604.19 | 5.142 | 558.59 | 0.0% | 1.96 | `point reads`
|
||||||
| 163.08 | 6,132,046.57 | 0.1% | 3,036.76 | 599.68 | 5.064 | 539.35 | 0.0% | 1.95 | `prefix reads`
|
| 161.05 | 6,209,395.38 | 0.1% | 3,036.76 | 592.21 | 5.128 | 539.35 | 0.0% | 1.93 | `prefix reads`
|
||||||
| 250.26 | 3,995,834.21 | 0.1% | 3,722.71 | 920.25 | 4.045 | 692.00 | 0.0% | 2.99 | `range reads`
|
| 239.55 | 4,174,539.38 | 0.1% | 3,722.71 | 880.68 | 4.227 | 692.00 | 0.0% | 2.86 | `range reads`
|
||||||
| 359.64 | 2,780,581.63 | 0.1% | 4,523.64 | 1,322.63 | 3.420 | 720.22 | 2.1% | 4.30 | `point writes`
|
| 354.75 | 2,818,919.14 | 0.7% | 4,523.64 | 1,304.75 | 3.467 | 720.22 | 2.0% | 4.23 | `point writes`
|
||||||
| 358.20 | 2,791,726.93 | 0.1% | 4,484.57 | 1,317.23 | 3.405 | 705.00 | 2.0% | 4.28 | `prefix writes`
|
| 345.32 | 2,895,878.47 | 0.1% | 4,484.57 | 1,270.31 | 3.530 | 705.00 | 1.8% | 4.12 | `prefix writes`
|
||||||
| 193.91 | 5,157,081.29 | 0.1% | 2,224.10 | 713.33 | 3.118 | 377.17 | 3.3% | 2.32 | `range writes`
|
| 193.48 | 5,168,547.42 | 0.1% | 2,224.10 | 711.72 | 3.125 | 377.17 | 3.3% | 2.32 | `range writes`
|
||||||
| 422.03 | 2,369,513.38 | 1.1% | 6,855.96 | 1,551.55 | 4.419 | 1,227.82 | 1.4% | 0.05 | `monotonic increasing point writes`
|
| 404.89 | 2,469,777.50 | 2.4% | 6,855.96 | 1,489.70 | 4.602 | 1,227.82 | 1.3% | 0.05 | `monotonic increasing point writes`
|
||||||
| 142,467.00 | 7,019.17 | 3.8% | 812,047.33 | 508,535.40 | 1.597 | 151,246.50 | 1.0% | 0.01 | `worst case for radix tree`
|
| 134,231.80 | 7,449.80 | 1.9% | 812,045.25 | 495,770.40 | 1.638 | 151,246.50 | 0.9% | 0.01 | `worst case for radix tree`
|
||||||
| 37.66 | 26,554,217.74 | 0.8% | 701.00 | 138.80 | 5.050 | 102.00 | 0.0% | 0.01 | `create and destroy`
|
| 37.80 | 26,454,311.17 | 0.4% | 701.00 | 139.14 | 5.038 | 102.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.25 | 81,664,669.29 | 0.4% | 233.49 | 44.94 | 5.196 | 31.23 | 0.7% | 0.15 | `point reads`
|
| 12.89 | 77,565,115.56 | 0.1% | 244.55 | 47.43 | 5.155 | 34.21 | 0.6% | 0.15 | `point reads`
|
||||||
| 13.35 | 74,903,950.49 | 0.1% | 271.75 | 49.10 | 5.534 | 40.21 | 0.4% | 0.16 | `prefix reads`
|
| 15.11 | 66,162,047.76 | 0.1% | 297.79 | 55.60 | 5.356 | 43.23 | 0.4% | 0.18 | `prefix reads`
|
||||||
| 31.37 | 31,874,583.62 | 0.1% | 700.39 | 115.43 | 6.067 | 100.83 | 0.2% | 0.37 | `range reads`
|
| 36.29 | 27,559,358.29 | 0.1% | 783.16 | 133.44 | 5.869 | 109.52 | 0.2% | 0.43 | `range reads`
|
||||||
| 19.55 | 51,153,057.80 | 0.1% | 366.51 | 71.90 | 5.098 | 47.92 | 0.5% | 0.23 | `point writes`
|
| 20.53 | 48,719,405.55 | 0.1% | 381.81 | 75.51 | 5.057 | 51.04 | 0.5% | 0.25 | `point writes`
|
||||||
| 36.92 | 27,088,344.17 | 0.1% | 645.11 | 135.72 | 4.753 | 100.31 | 0.4% | 0.44 | `prefix writes`
|
| 39.37 | 25,402,042.40 | 0.1% | 685.00 | 144.83 | 4.730 | 106.72 | 0.3% | 0.47 | `prefix writes`
|
||||||
| 36.84 | 27,143,553.08 | 0.1% | 691.04 | 135.42 | 5.103 | 114.01 | 0.1% | 0.45 | `range writes`
|
| 43.78 | 22,843,841.63 | 0.1% | 800.40 | 161.06 | 4.970 | 127.36 | 0.1% | 0.53 | `range writes`
|
||||||
| 74.62 | 13,400,467.72 | 0.9% | 1,418.37 | 274.20 | 5.173 | 273.39 | 0.1% | 0.01 | `monotonic increasing point writes`
|
| 78.37 | 12,760,008.75 | 1.0% | 1,452.61 | 288.24 | 5.040 | 278.69 | 0.1% | 0.01 | `monotonic increasing point writes`
|
||||||
| 352,671.00 | 2,835.50 | 15.3% | 3,687,535.00 | 1,287,119.00 | 2.865 | 712,030.00 | 0.1% | 0.01 | :wavy_dash: `worst case for radix tree` (Unstable with ~1.0 iters. Increase `minEpochIterations` to e.g. 10)
|
| 322,885.50 | 3,097.07 | 1.5% | 4,362,382.00 | 1,183,852.00 | 3.685 | 765,301.00 | 0.1% | 0.01 | `worst case for radix tree`
|
||||||
| 96.63 | 10,348,729.42 | 1.2% | 1,766.00 | 355.53 | 4.967 | 283.00 | 0.0% | 0.01 | `create and destroy`
|
| 99.99 | 10,000,718.79 | 0.4% | 1,775.00 | 367.93 | 4.824 | 288.00 | 0.0% | 0.01 | `create and destroy`
|
||||||
|
|
||||||
# "Real data" test
|
# "Real data" test
|
||||||
|
|
||||||
@@ -51,13 +51,13 @@ Point queries only. Gc ratio is the ratio of time spent doing garbage collection
|
|||||||
## skip list
|
## skip list
|
||||||
|
|
||||||
```
|
```
|
||||||
Check: 4.69875 seconds, 347.017 MB/s, Add: 3.39475 seconds, 165.153 MB/s, Gc ratio: 38.7414%, Peak idle memory: 5.51852e+06
|
Check: 4.62967 seconds, 352.195 MB/s, Add: 3.34177 seconds, 167.771 MB/s, Gc ratio: 37.9399%, Peak idle memory: 5.51852e+06
|
||||||
```
|
```
|
||||||
|
|
||||||
## radix tree
|
## radix tree
|
||||||
|
|
||||||
```
|
```
|
||||||
Check: 0.976745 seconds, 1669.37 MB/s, Add: 1.20464 seconds, 465.411 MB/s, Gc ratio: 39.4158%, Peak idle memory: 2.0226e+06
|
Check: 1.00477 seconds, 1622.8 MB/s, Add: 1.21142 seconds, 462.808 MB/s, Gc ratio: 39.4716%, Peak idle memory: 2.0226e+06
|
||||||
```
|
```
|
||||||
|
|
||||||
## hash table
|
## hash table
|
||||||
@@ -65,6 +65,6 @@ Check: 0.976745 seconds, 1669.37 MB/s, Add: 1.20464 seconds, 465.411 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.864589 seconds, 1885.92 MB/s, Add: 0.634604 seconds, 883.47 MB/s, Gc ratio: 41.184%, Peak idle memory: 0
|
Check: 0.854254 seconds, 1908.74 MB/s, Add: 0.632626 seconds, 886.232 MB/s, Gc ratio: 41.0827%, Peak idle memory: 0
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
// Exercises the SHOW_MEMORY instrumentation, which the other tests don't
|
|
||||||
// compile with (this target passes -DSHOW_MEMORY=1). The instrumentation has
|
|
||||||
// its own consistency checks, which abort on failure, and asserts are enabled
|
|
||||||
// by TEST_FLAGS. Only one ConflictSet may exist at a time, since the
|
|
||||||
// accounting is process-wide.
|
|
||||||
#include "ConflictSet.h"
|
|
||||||
#include "Internal.h"
|
|
||||||
|
|
||||||
#include <cassert>
|
|
||||||
#include <cstdint>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
using namespace weaselab;
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
void writePoint(ConflictSet &cs, const std::string &key, int64_t writeVersion) {
|
|
||||||
ConflictSet::WriteRange w{{(const uint8_t *)key.data(), int(key.size())},
|
|
||||||
{nullptr, 0}};
|
|
||||||
cs.addWrites(&w, 1, writeVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
ConflictSet::Result checkPoint(const ConflictSet &cs, const std::string &key,
|
|
||||||
int64_t readVersion) {
|
|
||||||
ConflictSet::ReadRange r{{(const uint8_t *)key.data(), int(key.size())},
|
|
||||||
{nullptr, 0},
|
|
||||||
readVersion};
|
|
||||||
ConflictSet::Result result;
|
|
||||||
cs.check(&r, &result, 1);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Everything a ConflictSet allocated must have been accounted for and released
|
|
||||||
// by the time it's destroyed.
|
|
||||||
void checkFullyReleased() {
|
|
||||||
assert(nodeBytes == 0);
|
|
||||||
assert(partialCapacityBytes == 0);
|
|
||||||
assert(totalKeys == 0);
|
|
||||||
assert(keyBytes == 0);
|
|
||||||
assert(mallocBytes == 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
int main() {
|
|
||||||
// Splitting a node replaces it, which defers the release of the old node.
|
|
||||||
// Releasing a deferred node must account for its capacity, and the key it
|
|
||||||
// held must be accounted for without walking `parent` pointers of nodes that
|
|
||||||
// have already been released.
|
|
||||||
{
|
|
||||||
ConflictSet cs{0};
|
|
||||||
writePoint(cs, "a", 1);
|
|
||||||
writePoint(cs, "ab", 2);
|
|
||||||
assert(checkPoint(cs, "a", 0) == ConflictSet::Conflict);
|
|
||||||
assert(checkPoint(cs, "ab", 0) == ConflictSet::Conflict);
|
|
||||||
assert(checkPoint(cs, "b", 0) == ConflictSet::Commit);
|
|
||||||
assert(nodeBytes > 0);
|
|
||||||
assert(keyBytes > 0);
|
|
||||||
}
|
|
||||||
checkFullyReleased();
|
|
||||||
|
|
||||||
// Exceeding the version window erases the tree mid-workload.
|
|
||||||
{
|
|
||||||
ConflictSet cs{0};
|
|
||||||
writePoint(cs, "abc", 1);
|
|
||||||
const int64_t version = int64_t(1) << 40;
|
|
||||||
writePoint(cs, "abc", version);
|
|
||||||
assert(checkPoint(cs, "abc", 0) == ConflictSet::TooOld);
|
|
||||||
assert(checkPoint(cs, "abc", version) == ConflictSet::Commit);
|
|
||||||
}
|
|
||||||
checkFullyReleased();
|
|
||||||
|
|
||||||
// Erase a tree with many nodes, both mid-workload and at destruction.
|
|
||||||
{
|
|
||||||
ConflictSet cs{0};
|
|
||||||
for (int i = 0; i < 1000; ++i) {
|
|
||||||
writePoint(cs, "prefix/" + std::to_string(i), i + 1);
|
|
||||||
}
|
|
||||||
const int64_t version = int64_t(1) << 40;
|
|
||||||
writePoint(cs, "prefix/1", version);
|
|
||||||
assert(checkPoint(cs, "prefix/1", version) == ConflictSet::Commit);
|
|
||||||
|
|
||||||
for (int i = 0; i < 1000; ++i) {
|
|
||||||
writePoint(cs, "other/" + std::to_string(i), version + i + 1);
|
|
||||||
}
|
|
||||||
assert(checkPoint(cs, "other/999", version) == ConflictSet::Conflict);
|
|
||||||
}
|
|
||||||
checkFullyReleased();
|
|
||||||
|
|
||||||
// Grow a node through every node type, so that the accounting for each node
|
|
||||||
// type's size is exercised, then erase the entries one at a time so that
|
|
||||||
// nodes are released individually rather than as part of a whole tree.
|
|
||||||
{
|
|
||||||
ConflictSet cs{0};
|
|
||||||
std::string key(1, '\0');
|
|
||||||
for (int i = 0; i < 256; ++i) {
|
|
||||||
key[0] = char(i);
|
|
||||||
writePoint(cs, key, i + 1);
|
|
||||||
}
|
|
||||||
for (int i = 0; i < 256; ++i) {
|
|
||||||
key[0] = char(i);
|
|
||||||
assert(checkPoint(cs, key, i) == ConflictSet::Conflict);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Garbage collection erases entries that can only fail with TooOld anyway.
|
|
||||||
// Doing it one version at a time walks the tree, erasing, downsizing and
|
|
||||||
// releasing nodes as it goes.
|
|
||||||
for (int i = 0; i < 256; ++i) {
|
|
||||||
cs.setOldestVersion(i + 1);
|
|
||||||
}
|
|
||||||
key[0] = '\0';
|
|
||||||
assert(checkPoint(cs, key, 0) == ConflictSet::TooOld);
|
|
||||||
}
|
|
||||||
checkFullyReleased();
|
|
||||||
}
|
|
||||||
+1
-8
@@ -819,17 +819,10 @@ struct __attribute__((visibility("hidden"))) ConflictSet::Impl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void setOldestVersion(int64_t oldestVersion) {
|
void setOldestVersion(int64_t oldestVersion) {
|
||||||
// oldestVersion becomes the max of its previous value and the argument, so
|
|
||||||
// an older version is a no-op. We still fall through in the equal case so
|
|
||||||
// that garbage collection can make progress without advancing the oldest
|
|
||||||
// version.
|
|
||||||
if (oldestVersion < this->oldestVersion) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This isn't 100% accurate. It overcounts if you hit the end
|
// This isn't 100% accurate. It overcounts if you hit the end
|
||||||
gc_iterations_total.add(keyUpdates);
|
gc_iterations_total.add(keyUpdates);
|
||||||
|
|
||||||
|
assert(oldestVersion >= this->oldestVersion);
|
||||||
this->oldestVersion = oldestVersion;
|
this->oldestVersion = oldestVersion;
|
||||||
SkipList::Finger finger;
|
SkipList::Finger finger;
|
||||||
int temp;
|
int temp;
|
||||||
|
|||||||
@@ -35,5 +35,36 @@ int main(int argc, char **argv) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
ConflictSet::MetricsV1 *metrics;
|
||||||
|
int metricsCount;
|
||||||
|
driver1.cs.getMetricsV1(&metrics, &metricsCount);
|
||||||
|
printf("#################### METRICS for ConflictSet 1 for %s "
|
||||||
|
"####################\n",
|
||||||
|
argv[i]);
|
||||||
|
for (int i = 0; i < metricsCount; ++i) {
|
||||||
|
printf("# HELP %s %s\n", metrics[i].name, metrics[i].help);
|
||||||
|
printf("# TYPE %s %s\n", metrics[i].name,
|
||||||
|
metrics[i].type == metrics[i].Counter ? "counter" : "gauge");
|
||||||
|
printf("%s %g\n", metrics[i].name, metrics[i].getValue());
|
||||||
|
}
|
||||||
|
puts("");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
ConflictSet::MetricsV1 *metrics;
|
||||||
|
int metricsCount;
|
||||||
|
driver2.cs.getMetricsV1(&metrics, &metricsCount);
|
||||||
|
printf("#################### METRICS for ConflictSet 2 for %s "
|
||||||
|
"####################\n",
|
||||||
|
argv[i]);
|
||||||
|
for (int i = 0; i < metricsCount; ++i) {
|
||||||
|
printf("# HELP %s %s\n", metrics[i].name, metrics[i].help);
|
||||||
|
printf("# TYPE %s %s\n", metrics[i].name,
|
||||||
|
metrics[i].type == metrics[i].Counter ? "counter" : "gauge");
|
||||||
|
printf("%s %g\n", metrics[i].name, metrics[i].getValue());
|
||||||
|
}
|
||||||
|
puts("");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <limits>
|
|
||||||
|
|
||||||
using namespace weaselab;
|
using namespace weaselab;
|
||||||
|
|
||||||
@@ -24,28 +23,6 @@ int main(void) {
|
|||||||
int64_t bytes = cs.getBytes();
|
int64_t bytes = cs.getBytes();
|
||||||
assert(bytes > 0);
|
assert(bytes > 0);
|
||||||
|
|
||||||
// Regression: a write just below INT64_MAX must not overflow the nominal
|
|
||||||
// version window comparison and erase entries that reads still observe.
|
|
||||||
// Versions are only required to be >= 0, so this is valid input.
|
|
||||||
{
|
|
||||||
const int64_t imax = std::numeric_limits<int64_t>::max();
|
|
||||||
const int64_t v0 = imax - int64_t(1000000000);
|
|
||||||
ConflictSet nearMax(0);
|
|
||||||
ConflictSet::WriteRange w2;
|
|
||||||
w2.begin.p = (const uint8_t *)"key";
|
|
||||||
w2.begin.len = 3;
|
|
||||||
w2.end.len = 0;
|
|
||||||
nearMax.addWrites(&w2, 1, v0);
|
|
||||||
nearMax.addWrites(&w2, 0, imax);
|
|
||||||
ConflictSet::ReadRange r2;
|
|
||||||
r2.begin.p = (const uint8_t *)"key";
|
|
||||||
r2.begin.len = 3;
|
|
||||||
r2.end.len = 0;
|
|
||||||
r2.readVersion = v0 - 1;
|
|
||||||
nearMax.check(&r2, &result, 1);
|
|
||||||
assert(result == ConflictSet::Conflict);
|
|
||||||
}
|
|
||||||
|
|
||||||
ConflictSet::MetricsV1 *metrics;
|
ConflictSet::MetricsV1 *metrics;
|
||||||
int metricsCount;
|
int metricsCount;
|
||||||
cs.getMetricsV1(&metrics, &metricsCount);
|
cs.getMetricsV1(&metrics, &metricsCount);
|
||||||
|
|||||||
+36
-69
@@ -21,12 +21,7 @@ limitations under the License.
|
|||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
namespace weaselab {
|
namespace weaselab {
|
||||||
/** A data structure for optimistic concurrency control on ranges of
|
/** A data structure for optimistic concurrency control on ranges of
|
||||||
* bitwise-lexicographically-ordered keys.
|
* bitwise-lexicographically-ordered keys. All versions must be >= 0.
|
||||||
*
|
|
||||||
* All versions are >= 0. `oldestVersion` is the smallest version a subsequent
|
|
||||||
* `check` can read and `newestVersion` is the largest version passed to
|
|
||||||
* `addWrites` or the constructor. `oldestVersion <= newestVersion`, and
|
|
||||||
* `oldestVersion >= newestVersion - two billion`.
|
|
||||||
*
|
*
|
||||||
* Thread safety:
|
* Thread safety:
|
||||||
* - It's safe to operate on two different ConflictSets in two different
|
* - It's safe to operate on two different ConflictSets in two different
|
||||||
@@ -41,7 +36,8 @@ struct __attribute__((__visibility__("default"))) ConflictSet {
|
|||||||
/** The result of a check which intersects a write at a version >
|
/** The result of a check which intersects a write at a version >
|
||||||
readVersion */
|
readVersion */
|
||||||
Conflict,
|
Conflict,
|
||||||
/** The result of a check with a readVersion older than `oldestVersion` */
|
/** The result of a check with a readVersion older than the highest call to
|
||||||
|
`setOldestVersion` */
|
||||||
TooOld
|
TooOld
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -58,6 +54,9 @@ struct __attribute__((__visibility__("default"))) ConflictSet {
|
|||||||
/** `end` having length 0 denotes that this range is the single key {begin}.
|
/** `end` having length 0 denotes that this range is the single key {begin}.
|
||||||
* Otherwise this denotes the range [begin, end), and begin must be < end */
|
* Otherwise this denotes the range [begin, end), and begin must be < end */
|
||||||
Key end;
|
Key end;
|
||||||
|
/** `readVersion` older than the oldestVersion or the version of the
|
||||||
|
* latest call to `addWrites` minus two billion will result in `TooOld`.
|
||||||
|
* Must be <= the version of the latest call to `addWrites` */
|
||||||
int64_t readVersion;
|
int64_t readVersion;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -69,38 +68,23 @@ struct __attribute__((__visibility__("default"))) ConflictSet {
|
|||||||
Key end;
|
Key end;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** The result of checking reads[i] is written in results[i].
|
/** The result of checking reads[i] is written in results[i] */
|
||||||
* @pre `reads` and `results` each hold at least `count` elements; each
|
|
||||||
* `reads[i]` is a valid `ReadRange` (see its field documentation); each
|
|
||||||
* `reads[i].readVersion <= newestVersion`.
|
|
||||||
* @post For every i < count, `results[i]` is `TooOld` if
|
|
||||||
* `reads[i].readVersion < oldestVersion`; `Conflict` if any write at a
|
|
||||||
* version greater than `reads[i].readVersion` intersects the range
|
|
||||||
* `reads[i]`; and `Commit` otherwise. */
|
|
||||||
void check(const ReadRange *reads, Result *results, int count) const;
|
void check(const ReadRange *reads, Result *results, int count) const;
|
||||||
|
|
||||||
/** Records writes at `writeVersion`, making them visible to `check`.
|
/** Reads intersecting writes where readVersion < `writeVersion` will result
|
||||||
* @pre `writes` holds at least `count` elements; every `writes[i]` is a valid
|
* in `Conflict` (or `TooOld`, eventually). `writeVersion` must be greater
|
||||||
* `WriteRange` (see its field documentation); `count >= 0`; `writeVersion >=
|
* than or equal to all previous write versions. Call `addWrites` with `count`
|
||||||
* newestVersion`.
|
* zero to only advance the version. */
|
||||||
* @post Writes (writes[i], writeVersion) are recorded for every i < count,
|
|
||||||
* and `newestVersion` becomes `writeVersion`. `oldestVersion` advances only
|
|
||||||
* as far as necessary: it becomes the larger of its previous value and
|
|
||||||
* `newestVersion - two billion`. Calling with `count == 0` adds no write;
|
|
||||||
* `newestVersion` (and possibly `oldestVersion`) still advance. */
|
|
||||||
void addWrites(const WriteRange *writes, int count, int64_t writeVersion);
|
void addWrites(const WriteRange *writes, int count, int64_t writeVersion);
|
||||||
|
|
||||||
/** Reads where readVersion < `oldestVersion` will result in `TooOld`.
|
/** Reads where readVersion < `oldestVersion` will result in `TooOld`. Must be
|
||||||
* @pre `oldestVersion >= 0` and `oldestVersion <= newestVersion`.
|
* greater than or equal to all previous oldest versions. Must be <= the
|
||||||
* @post `oldestVersion` becomes the maximum of its previous value and the
|
* version of the latest call to `addWrites` */
|
||||||
* argument; `newestVersion` is unchanged. */
|
|
||||||
void setOldestVersion(int64_t oldestVersion);
|
void setOldestVersion(int64_t oldestVersion);
|
||||||
|
|
||||||
/** Reads where readVersion < `oldestVersion` will result in `TooOld`.
|
/** Reads where readVersion < oldestVersion will result in `TooOld`. There are
|
||||||
* @pre `version >= 0`.
|
* no writes initially */
|
||||||
* @post No writes are recorded, and `oldestVersion == newestVersion ==
|
explicit ConflictSet(int64_t oldestVersion);
|
||||||
* version`. */
|
|
||||||
explicit ConflictSet(int64_t version);
|
|
||||||
|
|
||||||
~ConflictSet();
|
~ConflictSet();
|
||||||
|
|
||||||
@@ -162,12 +146,7 @@ private:
|
|||||||
#else
|
#else
|
||||||
|
|
||||||
/** A data structure for optimistic concurrency control on ranges of
|
/** A data structure for optimistic concurrency control on ranges of
|
||||||
* bitwise-lexicographically-ordered keys.
|
* bitwise-lexicographically-ordered keys. All versions must be >= 0.
|
||||||
*
|
|
||||||
* All versions are >= 0. `oldestVersion` is the smallest version a subsequent
|
|
||||||
* `check` can read and `newestVersion` is the largest version passed to
|
|
||||||
* `addWrites` or the constructor. `oldestVersion <= newestVersion`, and
|
|
||||||
* `oldestVersion >= newestVersion - two billion`.
|
|
||||||
*
|
*
|
||||||
* Thread safety:
|
* Thread safety:
|
||||||
* - It's safe to operate on two different ConflictSets in two different
|
* - It's safe to operate on two different ConflictSets in two different
|
||||||
@@ -181,10 +160,11 @@ typedef struct ConflictSet ConflictSet;
|
|||||||
typedef enum {
|
typedef enum {
|
||||||
/** The result of a check which does not intersect any conflicting writes */
|
/** The result of a check which does not intersect any conflicting writes */
|
||||||
ConflictSet_Commit,
|
ConflictSet_Commit,
|
||||||
/** The result of a check which intersects a write at a version >
|
/** The result of a check which intersects a write at a version > readVersion
|
||||||
readVersion */
|
*/
|
||||||
ConflictSet_Conflict,
|
ConflictSet_Conflict,
|
||||||
/** The result of a check with a readVersion older than `oldestVersion` */
|
/** The result of a check with a readVersion older than the highest call to
|
||||||
|
`setOldestVersion` */
|
||||||
ConflictSet_TooOld
|
ConflictSet_TooOld
|
||||||
} ConflictSet_Result;
|
} ConflictSet_Result;
|
||||||
|
|
||||||
@@ -201,6 +181,9 @@ typedef struct {
|
|||||||
/** `end` having length 0 denotes that this range is the single key {begin}.
|
/** `end` having length 0 denotes that this range is the single key {begin}.
|
||||||
* Otherwise this denotes the range [begin, end), and begin must be < end */
|
* Otherwise this denotes the range [begin, end), and begin must be < end */
|
||||||
ConflictSet_Key end;
|
ConflictSet_Key end;
|
||||||
|
/** `readVersion` older than the oldestVersion or the version of the
|
||||||
|
* latest call to `addWrites` minus two billion will result in `TooOld`.
|
||||||
|
* Must be <= the version of the latest call to `addWrites` */
|
||||||
int64_t readVersion;
|
int64_t readVersion;
|
||||||
} ConflictSet_ReadRange;
|
} ConflictSet_ReadRange;
|
||||||
|
|
||||||
@@ -212,43 +195,27 @@ typedef struct {
|
|||||||
ConflictSet_Key end;
|
ConflictSet_Key end;
|
||||||
} ConflictSet_WriteRange;
|
} ConflictSet_WriteRange;
|
||||||
|
|
||||||
/** The result of checking reads[i] is written in results[i].
|
/** The result of checking reads[i] is written in results[i] */
|
||||||
* @pre `reads` and `results` each hold at least `count` elements; each
|
|
||||||
* `reads[i]` is a valid `ConflictSet_ReadRange` (see its field documentation);
|
|
||||||
* each `reads[i].readVersion <= newestVersion`.
|
|
||||||
* @post For every i < count, `results[i]` is `ConflictSet_TooOld` if
|
|
||||||
* `reads[i].readVersion < oldestVersion`; `ConflictSet_Conflict` if any write
|
|
||||||
* at a version greater than `reads[i].readVersion` intersects the range
|
|
||||||
* `reads[i]`; and `ConflictSet_Commit` otherwise. */
|
|
||||||
void ConflictSet_check(const ConflictSet *cs,
|
void ConflictSet_check(const ConflictSet *cs,
|
||||||
const ConflictSet_ReadRange *reads,
|
const ConflictSet_ReadRange *reads,
|
||||||
ConflictSet_Result *results, int count);
|
ConflictSet_Result *results, int count);
|
||||||
|
|
||||||
/** Records writes at `writeVersion`, making them visible to
|
/** Reads intersecting writes where readVersion < `writeVersion` will result
|
||||||
* `ConflictSet_check`.
|
* in `Conflict` (or `TooOld`, eventually). `writeVersion` must be greater
|
||||||
* @pre `writes` holds at least `count` elements; every `writes[i]` is a valid
|
* than or equal to all previous write versions. Call `addWrites` with `count`
|
||||||
* `ConflictSet_WriteRange` (see its field documentation); `count >= 0`;
|
* zero to only advance the version. */
|
||||||
* `writeVersion >= newestVersion`.
|
|
||||||
* @post Writes (writes[i], writeVersion) are recorded for every i < count,
|
|
||||||
* and `newestVersion` becomes `writeVersion`. `oldestVersion` advances only
|
|
||||||
* as far as necessary: it becomes the larger of its previous value and
|
|
||||||
* `newestVersion - two billion`. Calling with `count == 0` adds no write;
|
|
||||||
* `newestVersion` (and possibly `oldestVersion`) still advance. */
|
|
||||||
void ConflictSet_addWrites(ConflictSet *cs,
|
void ConflictSet_addWrites(ConflictSet *cs,
|
||||||
const ConflictSet_WriteRange *writes, int count,
|
const ConflictSet_WriteRange *writes, int count,
|
||||||
int64_t writeVersion);
|
int64_t writeVersion);
|
||||||
|
|
||||||
/** Reads where readVersion < `oldestVersion` will result in `TooOld`.
|
/** Reads where readVersion < `oldestVersion` will result in `TooOld`. Must be
|
||||||
* @pre `oldestVersion >= 0` and `oldestVersion <= newestVersion`.
|
* greater than or equal to all previous oldest versions. Must be <= the
|
||||||
* @post `oldestVersion` becomes the maximum of its previous value and the
|
* version of the latest call to `addWrites` */
|
||||||
* argument; `newestVersion` is unchanged. */
|
|
||||||
void ConflictSet_setOldestVersion(ConflictSet *cs, int64_t oldestVersion);
|
void ConflictSet_setOldestVersion(ConflictSet *cs, int64_t oldestVersion);
|
||||||
|
|
||||||
/** Reads where readVersion < `oldestVersion` will result in `TooOld`.
|
/** Reads where readVersion < oldestVersion will result in `TooOld`. There are
|
||||||
* @pre `version >= 0`.
|
* no writes initially */
|
||||||
* @post No writes are recorded, and `oldestVersion == newestVersion ==
|
ConflictSet *ConflictSet_create(int64_t oldestVersion);
|
||||||
* version`. */
|
|
||||||
ConflictSet *ConflictSet_create(int64_t version);
|
|
||||||
|
|
||||||
void ConflictSet_destroy(ConflictSet *cs);
|
void ConflictSet_destroy(ConflictSet *cs);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#if defined(__x86_64__) && !defined(USE_SIMD_FALLBACK)
|
||||||
|
|
||||||
|
// SIMD operations on potentially-indeterminate Node16::index[16] bytes.
|
||||||
|
// Implemented in file-level assembly (simd_x86_64.S) because loading and
|
||||||
|
// operating on indeterminate values is UB in C++ but well-defined in
|
||||||
|
// assembly. The caller must mask the returned bitfield to
|
||||||
|
// [0, numChildren) before using it.
|
||||||
|
//
|
||||||
|
// Each function returns a 16-bit bitmask in the low 16 bits of a uint32_t
|
||||||
|
// (upper 16 bits are zero). Bit i is set iff the condition holds at index i.
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
// Returns bit i set iff idx[i] == key
|
||||||
|
uint32_t find_eq_16(const uint8_t idx[16], uint8_t key);
|
||||||
|
// Returns bit i set iff idx[i] >= child
|
||||||
|
uint32_t find_ge_16(const uint8_t idx[16], uint8_t child);
|
||||||
|
// Returns bit i set iff begin <= idx[i] < end
|
||||||
|
uint32_t mask_in_range_16(const uint8_t idx[16], uint8_t begin, uint8_t end);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
// SIMD operations on potentially-indeterminate Node16::index[16] bytes.
|
||||||
|
// Written in assembly so msan doesn't track the loads. The caller is
|
||||||
|
// responsible for masking the returned bitfield to [0, numChildren) before
|
||||||
|
// using it.
|
||||||
|
//
|
||||||
|
// All functions return a 16-bit bitmask in %eax (bit i set = condition true
|
||||||
|
// at index i). The upper 16 bits of %eax are zero.
|
||||||
|
//
|
||||||
|
// System V AMD64 ABI:
|
||||||
|
// %rdi = const uint8_t *idx (16 bytes)
|
||||||
|
// %esi = uint8_t key (find_eq_16, find_ge_16)
|
||||||
|
// %sil = uint8_t begin (mask_in_range_16)
|
||||||
|
// %dl = uint8_t end (mask_in_range_16)
|
||||||
|
|
||||||
|
.text
|
||||||
|
|
||||||
|
// uint32_t find_eq_16(const uint8_t idx[16], uint8_t key)
|
||||||
|
// Returns bit i set if idx[i] == key
|
||||||
|
.globl find_eq_16
|
||||||
|
.type find_eq_16, @function
|
||||||
|
find_eq_16:
|
||||||
|
vmovd %esi, %xmm1 // broadcast key
|
||||||
|
vpbroadcastb %xmm1, %xmm1
|
||||||
|
vmovdqu (%rdi), %xmm0 // load 16 bytes (may contain indeterminate data)
|
||||||
|
vpcmpeqb %xmm0, %xmm1, %xmm0 // 0xff for each match
|
||||||
|
vpmovmskb %xmm0, %eax // 16-bit bitmask
|
||||||
|
movzwl %ax, %eax // zero-extend to 32 bits
|
||||||
|
ret
|
||||||
|
.size find_eq_16, .-find_eq_16
|
||||||
|
|
||||||
|
// uint32_t find_ge_16(const uint8_t idx[16], uint8_t child)
|
||||||
|
// Returns bit i set if idx[i] >= child
|
||||||
|
// x86 doesn't have a "compare unsigned >=" for bytes directly, so we use:
|
||||||
|
// min(key, idx[i]) == key iff idx[i] >= key
|
||||||
|
.globl find_ge_16
|
||||||
|
.type find_ge_16, @function
|
||||||
|
find_ge_16:
|
||||||
|
vmovd %esi, %xmm1
|
||||||
|
vpbroadcastb %xmm1, %xmm1 // key broadcast
|
||||||
|
vmovdqu (%rdi), %xmm0 // load 16 bytes
|
||||||
|
vpminub %xmm0, %xmm1, %xmm2 // min(key, idx[i])
|
||||||
|
vpcmpeqb %xmm2, %xmm1, %xmm0 // 0xff where min == key, i.e. idx[i] >= key
|
||||||
|
vpmovmskb %xmm0, %eax
|
||||||
|
movzwl %ax, %eax
|
||||||
|
ret
|
||||||
|
.size find_ge_16, .-find_ge_16
|
||||||
|
|
||||||
|
// uint32_t mask_in_range_16(const uint8_t idx[16], uint8_t begin, uint8_t end)
|
||||||
|
// Returns bit i set if begin <= idx[i] < end
|
||||||
|
// Logic: (idx[i] - begin) < (end - begin) [unsigned wrapping arithmetic]
|
||||||
|
// Equivalently: idx[i] - begin != max(idx[i] - begin, end - begin)
|
||||||
|
// i.e. idx[i] - begin is NOT equal to the saturated value.
|
||||||
|
// We compute: sub = idx - begin; result = (sub < (end-begin)) for each byte.
|
||||||
|
// Using: sub == max(sub, end-begin) means NOT in range.
|
||||||
|
// So: in_range = ~(movemask(cmpeq(sub, max(sub, range_size))))
|
||||||
|
.globl mask_in_range_16
|
||||||
|
.type mask_in_range_16, @function
|
||||||
|
mask_in_range_16:
|
||||||
|
vmovd %esi, %xmm1 // begin
|
||||||
|
vpbroadcastb %xmm1, %xmm1
|
||||||
|
vmovd %edx, %xmm2 // end
|
||||||
|
vpbroadcastb %xmm2, %xmm2
|
||||||
|
vmovdqu (%rdi), %xmm0 // load 16 bytes
|
||||||
|
vpsubb %xmm1, %xmm0, %xmm0 // idx - begin (wrapping)
|
||||||
|
vpsubb %xmm1, %xmm2, %xmm2 // end - begin (range size)
|
||||||
|
vpmaxub %xmm0, %xmm2, %xmm3 // max(idx-begin, range_size)
|
||||||
|
vpcmpeqb %xmm3, %xmm0, %xmm0 // 0xff where NOT in range
|
||||||
|
vpmovmskb %xmm0, %eax
|
||||||
|
not %eax // invert: 1 = in range
|
||||||
|
movzwl %ax, %eax
|
||||||
|
ret
|
||||||
|
.size mask_in_range_16, .-mask_in_range_16
|
||||||
|
|
||||||
|
.section .note.GNU-stack,"",@progbits
|
||||||
+5
-58
@@ -68,54 +68,6 @@ def test_hash_table_getBytes():
|
|||||||
assert cs.check(read(0, b"key")) == [Result.CONFLICT]
|
assert cs.check(read(0, b"key")) == [Result.CONFLICT]
|
||||||
|
|
||||||
|
|
||||||
def test_setOldestVersion_does_not_decrease():
|
|
||||||
# Regression test for issue #87: `setOldestVersion` sets oldestVersion to
|
|
||||||
# the maximum of its previous value and its argument, so passing an older
|
|
||||||
# version is valid input and must be a no-op. skip_list used to assert in
|
|
||||||
# Debug builds and, in Release builds, lower its oldest version, which
|
|
||||||
# resurrected a window of versions that callers were told resulted in
|
|
||||||
# TooOld.
|
|
||||||
with DebugConflictSet() as cs:
|
|
||||||
cs.addWrites(5, write(b"k"))
|
|
||||||
cs.setOldestVersion(5)
|
|
||||||
# Valid: `2 >= 0` and `2 <= newestVersion == 5`. oldestVersion stays 5,
|
|
||||||
# so 3 and 4 are still TooOld.
|
|
||||||
cs.setOldestVersion(2)
|
|
||||||
assert cs.check(read(3, b"k")) == [Result.TOO_OLD]
|
|
||||||
assert cs.check(read(4, b"k")) == [Result.TOO_OLD]
|
|
||||||
assert cs.check(read(5, b"k")) == [Result.COMMIT]
|
|
||||||
|
|
||||||
# Same, but with the entries at the old oldestVersion already garbage
|
|
||||||
# collected, so lowering oldestVersion would report Commit for a version
|
|
||||||
# that the caller was told resulted in TooOld.
|
|
||||||
with DebugConflictSet() as cs:
|
|
||||||
cs.addWrites(1, write(b"a"))
|
|
||||||
cs.addWrites(2) # No writes; advances newestVersion to 2
|
|
||||||
cs.setOldestVersion(2) # Garbage collects the write at version 1
|
|
||||||
cs.setOldestVersion(1)
|
|
||||||
assert cs.check(read(1, b"a")) == [Result.TOO_OLD]
|
|
||||||
assert cs.check(read(2, b"a")) == [Result.COMMIT]
|
|
||||||
|
|
||||||
|
|
||||||
def test_hash_table_setOldestVersion_empties_map():
|
|
||||||
# Regression test for issue #78: setOldestVersion must terminate when
|
|
||||||
# its garbage-collection pass erases every entry in the map. Each write
|
|
||||||
# charges keyUpdates += 2, so with 50 writes keyUpdates = 100 and GC is
|
|
||||||
# enabled; erasing all entries leaves keyUpdates > 0 while the map is
|
|
||||||
# empty, which previously caused the GC loop to spin forever.
|
|
||||||
with ConflictSet(0, build_dir=build_dir, implementation="hash_table") as cs:
|
|
||||||
for i in range(50):
|
|
||||||
cs.addWrites(1, write(("k%06d" % i).encode()))
|
|
||||||
# All entries have version 1 <= oldestVersion 1 -> all erased.
|
|
||||||
cs.setOldestVersion(1)
|
|
||||||
# If we reach this point, setOldestVersion returned instead of hanging.
|
|
||||||
# The keys were erased, so reads at a valid version commit.
|
|
||||||
assert cs.check(read(1, b"k000000")) == [Result.COMMIT]
|
|
||||||
assert cs.check(read(2, b"k000000")) == [Result.COMMIT]
|
|
||||||
# A read older than oldestVersion is TooOld.
|
|
||||||
assert cs.check(read(0, b"k000000")) == [Result.TOO_OLD]
|
|
||||||
|
|
||||||
|
|
||||||
def test_write_read_without_outer_reference():
|
def test_write_read_without_outer_reference():
|
||||||
# Regression test for issue #42: WriteRange/ReadRange must keep their
|
# Regression test for issue #42: WriteRange/ReadRange must keep their
|
||||||
# backing key buffers alive, because the C library reads the pointer
|
# backing key buffers alive, because the C library reads the pointer
|
||||||
@@ -251,18 +203,13 @@ def test_merge_child_node48():
|
|||||||
cs.addWrites(1, write(b"\x00" * 8, b"\x00" * 10))
|
cs.addWrites(1, write(b"\x00" * 8, b"\x00" * 10))
|
||||||
|
|
||||||
|
|
||||||
def test_fixup_256_heterogeneous_versions():
|
def test_fixup_256():
|
||||||
# Regression test for issue #89: recomputing the maxVersion of a Node256
|
|
||||||
# must consider all of its children, not just those with an index < 16.
|
|
||||||
# Here the highest-version child ("\x10\x50") has an index >= 16, and the
|
|
||||||
# range write ends at the Node256, which erases that child from the range
|
|
||||||
# and recomputes the Node256's maxVersion.
|
|
||||||
with DebugConflictSet() as cs:
|
with DebugConflictSet() as cs:
|
||||||
|
cs.addWrites(0, write(bytes([1])))
|
||||||
for i in range(256):
|
for i in range(256):
|
||||||
cs.addWrites(1, write(bytes([0x10, i])))
|
cs.addWrites(1, write(bytes([1, i])))
|
||||||
cs.addWrites(10, write(bytes([0x10, 0x50])))
|
cs.addWrites(2, write(bytes([0]), bytes([1])))
|
||||||
cs.addWrites(11, write(bytes([0x05]), bytes([0x10])))
|
cs.check(read(0, bytes([1]), bytes([2])))
|
||||||
assert cs.check(read(5, bytes([0x10, 0x50]))) == [Result.CONFLICT]
|
|
||||||
|
|
||||||
|
|
||||||
def test_large_removal_buffer():
|
def test_large_removal_buffer():
|
||||||
|
|||||||
+1
-2
@@ -1,8 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# Disable frequency scaling
|
# Disable frequency scaling
|
||||||
# shellcheck disable=SC2002
|
for i in $(seq "$(nproc)") ; do sudo cat /sys/devices/system/cpu/cpu$((i-1))/cpufreq/scaling_max_freq | sudo tee /sys/devices/system/cpu/cpu$((i-1))/cpufreq/scaling_min_freq >/dev/null ; done
|
||||||
for i in $(seq "$(nproc)") ; do cat /sys/devices/system/cpu/cpu$((i-1))/cpufreq/scaling_max_freq | sudo tee /sys/devices/system/cpu/cpu$((i-1))/cpufreq/scaling_min_freq >/dev/null ; done
|
|
||||||
|
|
||||||
cmake -S. -Bbuild -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=clang++ -G Ninja -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DCMAKE_CXX_FLAGS=-DNVALGRIND > /dev/null
|
cmake -S. -Bbuild -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=clang++ -G Ninja -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DCMAKE_CXX_FLAGS=-DNVALGRIND > /dev/null
|
||||||
cmake --build build > /dev/null
|
cmake --build build > /dev/null
|
||||||
|
|||||||
Reference in new issue
Block a user