Skip to content

Commit 05459ba

Browse files
committed
feat: decouple allocator from QSBR, route through allocator_type (#837)
- Introduce allocator_type struct with alloc/dealloc/defer_dealloc - Wire destroy_callback through QSBR deallocation_request - Add UNODB_DETAIL_QSBR_ASSERT macro - Remove value_view alias from db types (keep get_result) - Add test_olc_no_qsbr with insert/delete exercise - Add test_art_allocator unit tests - Place allocator_ on separate cache line to avoid false sharing - Skip benchmarks for ASan builds (TODO #853: split into phases) - default_allocator: defer_dealloc=nullptr (db/mutex_db never call it) Closes #837
1 parent 36e4770 commit 05459ba

16 files changed

Lines changed: 422 additions & 126 deletions

.github/workflows/build.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -509,7 +509,10 @@ jobs:
509509
- name: Benchmark correctness test
510510
working-directory: ${{github.workspace}}/build
511511
run: make -k quick_benchmarks
512-
if: env.STATIC_ANALYSIS != 'ON'
512+
# TODO(#853): split benchmarks into phases instead of skipping
513+
if: >
514+
env.STATIC_ANALYSIS != 'ON' &&
515+
env.SANITIZE_ADDRESS != 'ON'
513516
514517
- name: DeepState 1 minute fuzzing
515518
working-directory: ${{github.workspace}}/build

CMakeLists.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -712,6 +712,7 @@ function(COMMON_TARGET_PROPERTIES TARGET)
712712
"$<${use_boost_stacktrace}:UNODB_DETAIL_BOOST_STACKTRACE>"
713713
"$<${with_stats}:UNODB_DETAIL_WITH_STATS>"
714714
"$<${coverage_on}:UNODB_COVERAGE>"
715+
"$<${is_not_release_genex}:UNODB_DETAIL_QSBR_DEBUG>"
715716
"UNODB_SPINLOCK_LOOP_VALUE=${SPINLOCK_LOOP_VALUE}")
716717
target_compile_options(${TARGET} PUBLIC
717718
# Architecture
@@ -823,7 +824,8 @@ if(LIBFUZZER_AVAILABLE)
823824
target_link_libraries(unodb_qsbr_lf PUBLIC unodb_util Threads::Threads)
824825
endif()
825826

826-
add_unodb_library(unodb art.hpp art_common.hpp mutex_art.hpp optimistic_lock.hpp
827+
add_unodb_library(unodb art.hpp art_common.hpp art_allocator.hpp
828+
mutex_art.hpp optimistic_lock.hpp
827829
art_internal_impl.hpp olc_art.hpp art_internal.hpp art_internal.cpp
828830
node_type.hpp duckdb_encode_decode.hpp)
829831
target_link_libraries(unodb PUBLIC unodb_util unodb_qsbr)

art.hpp

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
#include <boost/container/small_vector.hpp>
2727

28+
#include "art_allocator.hpp"
2829
#include "art_common.hpp"
2930
#include "art_internal.hpp"
3031
#include "art_internal_impl.hpp"
@@ -130,12 +131,9 @@ class db final {
130131
/// The type of the value associated with the keys in the index.
131132
using value_type = Value;
132133

133-
/// View type for values stored in the index.
134-
using value_view = unodb::value_view;
135-
136134
/// Result type for get operations.
137135
///
138-
/// Contains value_view if key was found, otherwise empty.
136+
/// Contains the value if key was found, otherwise empty.
139137
using get_result = std::optional<value_type>;
140138

141139
/// Base class type for internal nodes.
@@ -206,9 +204,16 @@ class db final {
206204
public:
207205
// Creation and destruction
208206

209-
/// Construct empty ART index.
207+
/// Construct empty ART index with default allocator.
210208
db() noexcept = default;
211209

210+
/// Construct empty ART index with a custom allocator.
211+
constexpr explicit db(const allocator_type& alloc) noexcept
212+
: allocator_{alloc} {
213+
UNODB_DETAIL_ASSERT(allocator_.alloc != nullptr);
214+
UNODB_DETAIL_ASSERT(allocator_.dealloc != nullptr);
215+
}
216+
212217
/// Destroy ART index, freeing all tree nodes.
213218
~db() noexcept;
214219

@@ -242,6 +247,11 @@ class db final {
242247
return root == nullptr;
243248
}
244249

250+
/// Return the allocator used by this tree.
251+
[[nodiscard]] constexpr const allocator_type& get_allocator() const noexcept {
252+
return allocator_;
253+
}
254+
245255
/// Insert a value under a key iff there is no entry for that key.
246256
///
247257
/// \param insert_key If Key is a simple primitive type, then it is converted
@@ -924,6 +934,9 @@ class db final {
924934
/// Root of the tree (nullptr if empty).
925935
detail::node_ptr root{nullptr};
926936

937+
/// Allocator for tree nodes.
938+
allocator_type allocator_{detail::default_allocator};
939+
927940
#ifdef UNODB_DETAIL_WITH_STATS
928941

929942
/// Current memory use by all tree nodes in bytes.

art_allocator.hpp

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Copyright 2026 UnoDB contributors
2+
#ifndef UNODB_DETAIL_ART_ALLOCATOR_HPP
3+
#define UNODB_DETAIL_ART_ALLOCATOR_HPP
4+
5+
/// \file
6+
/// Pluggable allocator for ART trees (header-only, no QSBR dependency).
7+
///
8+
/// Trees accept an allocator_type at construction time. The default
9+
/// allocator uses the built-in aligned heap (heap.hpp) with immediate
10+
/// deferred free. OLC trees override defer_dealloc with a QSBR-based
11+
/// implementation. External integrators can supply their own allocator
12+
/// to route allocations through a custom heap and deferred free through
13+
/// an external epoch-based GC system.
14+
///
15+
/// \see https://github.com/unodb-dev/unodb/issues/837
16+
17+
// Should be the first include
18+
#include "global.hpp"
19+
20+
#include <cstddef>
21+
22+
#include "heap.hpp"
23+
24+
namespace unodb {
25+
26+
/// Callback invoked when a deferred deallocation is safe to execute.
27+
using destroy_callback_type = void (*)(void* ptr, std::size_t size, void* ctx);
28+
29+
/// Pluggable allocator for ART trees.
30+
///
31+
/// All three function pointers must be non-null. \a ctx is forwarded
32+
/// to every callback and may be nullptr.
33+
///
34+
/// \a defer_dealloc is called when a node is removed and cannot be freed
35+
/// immediately (concurrent readers may hold pointers). For single-threaded
36+
/// trees the default calls \a destroy_callback immediately. OLC trees
37+
/// replace this with QSBR-based deferred reclamation.
38+
struct allocator_type {
39+
/// Allocate `size` bytes with the given `alignment`. May throw on failure.
40+
void* (*alloc)(std::size_t size, std::size_t alignment, void* ctx);
41+
/// Free a previously allocated block of `size` bytes at `ptr`.
42+
void (*dealloc)(void* ptr, std::size_t size, void* ctx);
43+
/// Schedule deferred deallocation of `ptr` (`size` bytes). Calls
44+
/// `destroy_callback` when reclamation is safe.
45+
void (*defer_dealloc)(void* ptr, std::size_t size,
46+
destroy_callback_type destroy_callback, void* ctx);
47+
/// Opaque context forwarded to all callbacks.
48+
void* ctx;
49+
};
50+
51+
namespace detail {
52+
53+
/// Default alloc: delegates to allocate_aligned (heap.hpp).
54+
inline void* default_alloc(std::size_t size, std::size_t alignment,
55+
void* /*ctx*/) {
56+
return allocate_aligned(size, alignment);
57+
}
58+
59+
/// Default dealloc: delegates to free_aligned (heap.hpp).
60+
inline void default_dealloc(void* ptr, std::size_t /*size*/,
61+
void* /*ctx*/) noexcept {
62+
free_aligned(ptr);
63+
}
64+
65+
/// Default defer_dealloc: immediate free via destroy_callback.
66+
/// Safe for db and mutex_db where no concurrent readers exist.
67+
inline void default_defer_dealloc(void* ptr, std::size_t size,
68+
destroy_callback_type destroy_callback,
69+
void* ctx) noexcept {
70+
destroy_callback(ptr, size, ctx);
71+
}
72+
73+
/// Default destroy callback: frees via default_dealloc.
74+
/// Passed as the destroy_callback argument to defer_dealloc.
75+
inline void default_destroy(void* ptr, std::size_t size, void* ctx) noexcept {
76+
default_dealloc(ptr, size, ctx);
77+
}
78+
79+
/// The default allocator instance (no QSBR, immediate free).
80+
inline constexpr allocator_type default_allocator{
81+
&default_alloc, &default_dealloc, nullptr, nullptr};
82+
83+
} // namespace detail
84+
85+
} // namespace unodb
86+
87+
#endif // UNODB_DETAIL_ART_ALLOCATOR_HPP

art_internal_impl.hpp

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
#include <arm_neon.h>
3939
#endif
4040

41+
#include "art_allocator.hpp"
4142
#include "art_common.hpp"
4243
#include "art_internal.hpp"
4344
#include "assert.hpp"
@@ -318,17 +319,13 @@ class [[nodiscard]] basic_leaf final : public Header {
318319
UNODB_DETAIL_RESTORE_MSVC_WARNINGS()
319320
UNODB_DETAIL_RESTORE_MSVC_WARNINGS()
320321

321-
#ifdef UNODB_DETAIL_WITH_STATS
322-
323322
/// Return byte size of leaf data structure.
324323
///
325324
/// \return Size in bytes
326325
[[nodiscard, gnu::pure]] constexpr auto get_size() const noexcept {
327326
return compute_size(key_size, value_size);
328327
}
329328

330-
#endif // UNODB_DETAIL_WITH_STATS
331-
332329
/// Dump leaf contents to stream for debugging.
333330
///
334331
/// \param os Output stream
@@ -429,11 +426,10 @@ class [[nodiscard]] basic_leaf<no_key_tag, Header> final : public Header {
429426
UNODB_DETAIL_RESTORE_MSVC_WARNINGS()
430427
UNODB_DETAIL_RESTORE_MSVC_WARNINGS()
431428

432-
#ifdef UNODB_DETAIL_WITH_STATS
429+
/// Return byte size of keyless leaf data structure.
433430
[[nodiscard, gnu::pure]] constexpr auto get_size() const noexcept {
434431
return compute_size(value_size);
435432
}
436-
#endif
437433

438434
/// Dump keyless leaf contents to stream for debugging.
439435
[[gnu::cold]] UNODB_DETAIL_NOINLINE void dump(std::ostream& os,
@@ -523,8 +519,8 @@ template <typename Key, typename Value, template <typename, typename> class Db>
523519
leaf_val_bytes.size_bytes()));
524520
}
525521

526-
auto* const leaf_mem = static_cast<std::byte*>(
527-
allocate_aligned(size, alignment_for_new<leaf_type>()));
522+
auto* const leaf_mem = static_cast<std::byte*>(db.get_allocator().alloc(
523+
size, alignment_for_new<leaf_type>(), db.get_allocator().ctx));
528524

529525
#ifdef UNODB_DETAIL_WITH_STATS
530526
db.increment_leaf_count(size);
@@ -586,11 +582,9 @@ struct basic_inode_def final {
586582
template <class Db>
587583
inline void basic_db_leaf_deleter<Db>::operator()(
588584
leaf_type* to_delete) const noexcept {
589-
#ifdef UNODB_DETAIL_WITH_STATS
590585
const auto leaf_size = to_delete->get_size();
591-
#endif // UNODB_DETAIL_WITH_STATS
592-
593-
free_aligned(to_delete);
586+
const auto& alloc = db.get_allocator();
587+
alloc.dealloc(to_delete, leaf_size, alloc.ctx);
594588

595589
#ifdef UNODB_DETAIL_WITH_STATS
596590
db.decrement_leaf_count(leaf_size);
@@ -602,7 +596,8 @@ inline void basic_db_inode_deleter<INode, Db>::operator()(
602596
INode* inode_ptr) noexcept {
603597
static_assert(std::is_trivially_destructible_v<INode>);
604598

605-
free_aligned(inode_ptr);
599+
const auto& alloc = db.get_allocator();
600+
alloc.dealloc(inode_ptr, sizeof(INode), alloc.ctx);
606601

607602
#ifdef UNODB_DETAIL_WITH_STATS
608603
db.template decrement_inode_count<INode>();
@@ -852,8 +847,10 @@ struct basic_art_policy final {
852847
[[nodiscard]] static auto make_db_inode_unique_ptr(db_type& db_instance
853848
UNODB_DETAIL_LIFETIMEBOUND,
854849
Args&&... args) {
855-
auto* const inode_mem = static_cast<std::byte*>(
856-
allocate_aligned(sizeof(INode), alignment_for_new<INode>()));
850+
auto* const inode_mem =
851+
static_cast<std::byte*>(db_instance.get_allocator().alloc(
852+
sizeof(INode), alignment_for_new<INode>(),
853+
db_instance.get_allocator().ctx));
857854

858855
#ifdef UNODB_DETAIL_WITH_STATS
859856
db_instance.template increment_inode_count<INode>();

assert.hpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,15 @@ assert_failure(const char* file, int line, const char* func,
168168

169169
#endif // !defined(NDEBUG)
170170

171+
/// Assert that is only active when QSBR debug checking is enabled.
172+
/// Use for assertions that reference QSBR state (qsbr::instance(), etc.)
173+
/// which may not be available in builds without QSBR linked.
174+
#ifdef UNODB_DETAIL_QSBR_DEBUG
175+
#define UNODB_DETAIL_QSBR_ASSERT(condition) UNODB_DETAIL_ASSERT(condition)
176+
#else
177+
#define UNODB_DETAIL_QSBR_ASSERT(condition) ((void)0)
178+
#endif
179+
171180
UNODB_DETAIL_RESTORE_MSVC_WARNINGS()
172181

173182
} // namespace unodb::detail

fuzz_deepstate/test_qsbr_fuzz_deepstate.cpp

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2021-2025 Laurynas Biveinis
1+
// Copyright 2021-2026 UnoDB contributors
22

33
#include "global.hpp"
44

@@ -200,6 +200,11 @@ void check_qsbr_pointer_on_dealloc(const void* ptr) noexcept {
200200
}
201201
#endif
202202

203+
void fuzz_destroy_callback(void* ptr, std::size_t /*size*/,
204+
void* /*ctx*/) noexcept {
205+
unodb::detail::free_aligned(ptr);
206+
}
207+
203208
void deallocate_pointer(std::uint64_t* ptr) {
204209
ASSERT(!unodb::this_thread().is_qsbr_paused());
205210
ASSERT(*ptr == object_mem);
@@ -221,13 +226,9 @@ void deallocate_pointer(std::uint64_t* ptr) {
221226
bool op_completed;
222227
try {
223228
unodb::this_thread().on_next_epoch_deallocate(
224-
ptr
225-
#ifdef UNODB_DETAIL_WITH_STATS
226-
,
227-
sizeof(object_mem)
228-
#endif
229+
ptr, sizeof(object_mem), &fuzz_destroy_callback, nullptr
229230
#ifndef NDEBUG
230-
,
231+
,
231232
check_qsbr_pointer_on_dealloc
232233
#endif
233234
);

mutex_art.hpp

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ class mutex_db final {
2626
using key_type = Key;
2727
/// The type of the value associated with the keys in the index.
2828
using value_type = Value;
29-
using value_view = unodb::value_view;
3029

3130
/// If the search key was found, that is, the first pair member has a value,
3231
/// then the second member is a locked tree mutex which must be released ASAP
@@ -66,8 +65,19 @@ class mutex_db final {
6665

6766
public:
6867
// Creation and destruction
68+
69+
/// Construct empty mutex-protected ART index with default allocator.
6970
mutex_db() noexcept = default;
7071

72+
/// Construct empty mutex-protected ART index with a custom allocator.
73+
constexpr explicit mutex_db(const allocator_type& alloc) noexcept
74+
: db_{alloc} {}
75+
76+
/// Return the allocator used by this tree.
77+
[[nodiscard]] constexpr const allocator_type& get_allocator() const noexcept {
78+
return db_.get_allocator();
79+
}
80+
7181
/// Query for a value associated with a key.
7282
///
7383
/// \param search_key If Key is a simple primitive type, then it is converted

0 commit comments

Comments
 (0)