usearch 1.1.1 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/prerelease.yml +2 -21
- package/.github/workflows/release.yml +4 -36
- package/.github/workflows/update_version.sh +1 -1
- package/.gitmodules +1 -4
- package/Cargo.toml +1 -1
- package/Package.swift +2 -1
- package/README.md +86 -42
- package/binding.gyp +16 -12
- package/build.gradle +1 -1
- package/build.rs +0 -1
- package/c/CMakeLists.txt +11 -7
- package/c/lib.cpp +36 -24
- package/c/usearch.h +42 -12
- package/cluster.ipynb +167 -0
- package/conanfile.py +1 -1
- package/cpp/CMakeLists.txt +8 -3
- package/cpp/bench.cpp +2 -2
- package/cpp/test.cpp +42 -25
- package/docs/benchmarks.md +8 -0
- package/docs/compilation.md +7 -7
- package/docs/conf.py +27 -24
- package/docs/javascript/reference.rst +3 -0
- package/golang/README.md +31 -7
- package/golang/lib.go +1 -1
- package/include/usearch/index.hpp +348 -69
- package/include/usearch/index_dense.hpp +587 -179
- package/include/usearch/index_plugins.hpp +194 -16
- package/java/cloud/unum/usearch/Index.java +13 -0
- package/java/cloud/unum/usearch/cloud_unum_usearch_Index.cpp +40 -9
- package/java/cloud/unum/usearch/cloud_unum_usearch_Index.h +16 -0
- package/javascript/docs.js +111 -0
- package/javascript/lib.cpp +29 -9
- package/javascript/usearch.d.ts +9 -1
- package/objc/USearchObjective.mm +11 -11
- package/package.json +2 -2
- package/pyproject.toml +2 -2
- package/python/lib.cpp +488 -158
- package/python/scripts/index_faiss.py +3 -3
- package/python/scripts/join.py +6 -6
- package/python/scripts/test_index.py +219 -0
- package/python/scripts/test_sparse.py +21 -21
- package/python/scripts/test_tooling.py +111 -0
- package/python/usearch/eval.py +9 -5
- package/python/usearch/index.py +491 -216
- package/rust/lib.cpp +6 -24
- package/rust/lib.hpp +1 -3
- package/rust/lib.rs +89 -0
- package/setup.py +5 -6
- package/wasm/CMakeLists.txt +0 -1
- package/wasmer.toml +1 -1
- package/python/scripts/test.py +0 -364
- package/robin-map/.codecov.yml +0 -5
- package/robin-map/.github/workflows/ci.yml +0 -136
- package/robin-map/CMakeLists.txt +0 -86
- package/robin-map/LICENSE +0 -21
- package/robin-map/README.md +0 -483
- package/robin-map/cmake/tsl-robin-mapConfig.cmake.in +0 -9
- package/robin-map/doxygen.conf +0 -2483
- package/robin-map/include/tsl/robin_growth_policy.h +0 -406
- package/robin-map/include/tsl/robin_hash.h +0 -1639
- package/robin-map/include/tsl/robin_map.h +0 -807
- package/robin-map/include/tsl/robin_set.h +0 -660
- package/robin-map/tests/CMakeLists.txt +0 -26
- package/robin-map/tests/custom_allocator_tests.cpp +0 -138
- package/robin-map/tests/main.cpp +0 -26
- package/robin-map/tests/policy_tests.cpp +0 -97
- package/robin-map/tests/robin_map_tests.cpp +0 -1451
- package/robin-map/tests/robin_set_tests.cpp +0 -164
- package/robin-map/tests/utils.h +0 -443
- package/robin-map/tsl-robin-map.natvis +0 -78
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
#define USEARCH_DEFINED_CPP17
|
|
20
20
|
#endif
|
|
21
21
|
|
|
22
|
-
// Inferring target OS
|
|
22
|
+
// Inferring target OS: Windows, MacOS, or Linux
|
|
23
23
|
#if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__)
|
|
24
24
|
#define USEARCH_DEFINED_WINDOWS
|
|
25
25
|
#elif defined(__APPLE__) && defined(__MACH__)
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
#define USEARCH_DEFINED_LINUX
|
|
29
29
|
#endif
|
|
30
30
|
|
|
31
|
-
// Inferring the compiler
|
|
31
|
+
// Inferring the compiler: Clang vs GCC
|
|
32
32
|
#if defined(__clang__)
|
|
33
33
|
#define USEARCH_DEFINED_CLANG
|
|
34
34
|
#elif defined(__GNUC__)
|
|
@@ -324,6 +324,9 @@ class error_t {
|
|
|
324
324
|
message_ = message;
|
|
325
325
|
return *this;
|
|
326
326
|
}
|
|
327
|
+
|
|
328
|
+
error_t(error_t const&) = delete;
|
|
329
|
+
error_t& operator=(error_t const&) = delete;
|
|
327
330
|
error_t(error_t&& other) noexcept : message_(exchange(other.message_, nullptr)) {}
|
|
328
331
|
error_t& operator=(error_t&& other) noexcept {
|
|
329
332
|
std::swap(message_, other.message_);
|
|
@@ -331,6 +334,7 @@ class error_t {
|
|
|
331
334
|
}
|
|
332
335
|
explicit operator bool() const noexcept { return message_ != nullptr; }
|
|
333
336
|
char const* what() const noexcept { return message_; }
|
|
337
|
+
char const* release() noexcept { return exchange(message_, nullptr); }
|
|
334
338
|
|
|
335
339
|
#if defined(__cpp_exceptions) || defined(__EXCEPTIONS)
|
|
336
340
|
~error_t() noexcept(false) {
|
|
@@ -382,10 +386,10 @@ template <typename result_at> struct expected_gt {
|
|
|
382
386
|
};
|
|
383
387
|
|
|
384
388
|
/**
|
|
385
|
-
* @brief Light-weight bitset implementation to
|
|
389
|
+
* @brief Light-weight bitset implementation to sync nodes updates during graph mutations.
|
|
386
390
|
* Extends basic functionality with @b atomic operations.
|
|
387
391
|
*/
|
|
388
|
-
template <typename allocator_at = std::allocator<byte_t>> class
|
|
392
|
+
template <typename allocator_at = std::allocator<byte_t>> class bitset_gt {
|
|
389
393
|
using allocator_t = allocator_at;
|
|
390
394
|
using byte_t = typename allocator_t::value_type;
|
|
391
395
|
static_assert(sizeof(byte_t) == 1, "Allocator must allocate separate addressable bytes");
|
|
@@ -401,8 +405,8 @@ template <typename allocator_at = std::allocator<byte_t>> class visits_bitset_gt
|
|
|
401
405
|
std::size_t count_{};
|
|
402
406
|
|
|
403
407
|
public:
|
|
404
|
-
|
|
405
|
-
~
|
|
408
|
+
bitset_gt() noexcept {}
|
|
409
|
+
~bitset_gt() noexcept { reset(); }
|
|
406
410
|
|
|
407
411
|
explicit operator bool() const noexcept { return slots_; }
|
|
408
412
|
void clear() noexcept { std::memset(slots_, 0, count_ * sizeof(compressed_slot_t)); }
|
|
@@ -414,25 +418,25 @@ template <typename allocator_at = std::allocator<byte_t>> class visits_bitset_gt
|
|
|
414
418
|
count_ = 0;
|
|
415
419
|
}
|
|
416
420
|
|
|
417
|
-
|
|
421
|
+
bitset_gt(std::size_t capacity) noexcept
|
|
418
422
|
: slots_((compressed_slot_t*)allocator_t{}.allocate(slots(capacity) * sizeof(compressed_slot_t))),
|
|
419
423
|
count_(slots_ ? slots(capacity) : 0u) {
|
|
420
424
|
clear();
|
|
421
425
|
}
|
|
422
426
|
|
|
423
|
-
|
|
427
|
+
bitset_gt(bitset_gt&& other) noexcept {
|
|
424
428
|
slots_ = exchange(other.slots_, nullptr);
|
|
425
429
|
count_ = exchange(other.count_, 0);
|
|
426
430
|
}
|
|
427
431
|
|
|
428
|
-
|
|
432
|
+
bitset_gt& operator=(bitset_gt&& other) noexcept {
|
|
429
433
|
std::swap(slots_, other.slots_);
|
|
430
434
|
std::swap(count_, other.count_);
|
|
431
435
|
return *this;
|
|
432
436
|
}
|
|
433
437
|
|
|
434
|
-
|
|
435
|
-
|
|
438
|
+
bitset_gt(bitset_gt const&) = delete;
|
|
439
|
+
bitset_gt& operator=(bitset_gt const&) = delete;
|
|
436
440
|
|
|
437
441
|
inline bool test(std::size_t i) const noexcept { return slots_[i / bits_per_slot()] & (1ul << (i & bits_mask())); }
|
|
438
442
|
inline bool set(std::size_t i) noexcept {
|
|
@@ -470,13 +474,12 @@ template <typename allocator_at = std::allocator<byte_t>> class visits_bitset_gt
|
|
|
470
474
|
#endif
|
|
471
475
|
|
|
472
476
|
class lock_t {
|
|
473
|
-
|
|
477
|
+
bitset_gt& bitset_;
|
|
474
478
|
std::size_t bit_offset_;
|
|
475
479
|
|
|
476
480
|
public:
|
|
477
481
|
inline ~lock_t() noexcept { bitset_.atomic_reset(bit_offset_); }
|
|
478
|
-
inline lock_t(
|
|
479
|
-
: bitset_(bitset), bit_offset_(bit_offset) {
|
|
482
|
+
inline lock_t(bitset_gt& bitset, std::size_t bit_offset) noexcept : bitset_(bitset), bit_offset_(bit_offset) {
|
|
480
483
|
while (bitset_.atomic_set(bit_offset_))
|
|
481
484
|
;
|
|
482
485
|
}
|
|
@@ -485,7 +488,7 @@ template <typename allocator_at = std::allocator<byte_t>> class visits_bitset_gt
|
|
|
485
488
|
inline lock_t lock(std::size_t i) noexcept { return {*this, i}; }
|
|
486
489
|
};
|
|
487
490
|
|
|
488
|
-
using
|
|
491
|
+
using bitset_t = bitset_gt<>;
|
|
489
492
|
|
|
490
493
|
/**
|
|
491
494
|
* @brief Similar to `std::priority_queue`, but allows raw access to underlying
|
|
@@ -806,6 +809,149 @@ class usearch_pack_m uint40_t {
|
|
|
806
809
|
|
|
807
810
|
static_assert(sizeof(uint40_t) == 5, "uint40_t must be exactly 5 bytes");
|
|
808
811
|
|
|
812
|
+
// clang-format off
|
|
813
|
+
template <typename key_at, typename std::enable_if<std::is_integral<key_at>::value>::type* = nullptr> key_at default_free_value() { return std::numeric_limits<key_at>::max(); }
|
|
814
|
+
template <typename key_at, typename std::enable_if<std::is_same<key_at, uint40_t>::value>::type* = nullptr> uint40_t default_free_value() { return uint40_t::max(); }
|
|
815
|
+
template <typename key_at, typename std::enable_if<!std::is_integral<key_at>::value && !std::is_same<key_at, uint40_t>::value>::type* = nullptr> key_at default_free_value() { return key_at(); }
|
|
816
|
+
// clang-format on
|
|
817
|
+
|
|
818
|
+
template <typename element_at> struct hash_gt {
|
|
819
|
+
std::size_t operator()(element_at const& element) const noexcept { return std::hash<element_at>{}(element); }
|
|
820
|
+
};
|
|
821
|
+
|
|
822
|
+
template <> struct hash_gt<uint40_t> {
|
|
823
|
+
std::size_t operator()(uint40_t const& element) const noexcept { return std::hash<std::size_t>{}(element); }
|
|
824
|
+
};
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* @brief Minimalistic hash-set implementation to track visited nodes during graph traversal.
|
|
828
|
+
*
|
|
829
|
+
* It doesn't support deletion of separate objects, but supports `clear`-ing all at once.
|
|
830
|
+
* It expects `reserve` to be called ahead of all insertions, so no resizes are needed.
|
|
831
|
+
* It also assumes `0xFF...FF` slots to be unused, to simplify the design.
|
|
832
|
+
* It uses linear probing, the number of slots is always a power of two, and it uses linear-probing
|
|
833
|
+
* in case of bucket collisions.
|
|
834
|
+
*/
|
|
835
|
+
template <typename element_at, typename hasher_at = hash_gt<element_at>, typename allocator_at = std::allocator<byte_t>>
|
|
836
|
+
class growing_hash_set_gt {
|
|
837
|
+
|
|
838
|
+
using element_t = element_at;
|
|
839
|
+
using hasher_t = hasher_at;
|
|
840
|
+
|
|
841
|
+
using allocator_t = allocator_at;
|
|
842
|
+
using byte_t = typename allocator_t::value_type;
|
|
843
|
+
static_assert(sizeof(byte_t) == 1, "Allocator must allocate separate addressable bytes");
|
|
844
|
+
|
|
845
|
+
element_t* slots_{};
|
|
846
|
+
/// @brief Number of slots.
|
|
847
|
+
std::size_t capacity_{};
|
|
848
|
+
/// @brief Number of populated.
|
|
849
|
+
std::size_t count_{};
|
|
850
|
+
hasher_t hasher_{};
|
|
851
|
+
|
|
852
|
+
public:
|
|
853
|
+
growing_hash_set_gt() noexcept {}
|
|
854
|
+
~growing_hash_set_gt() noexcept { reset(); }
|
|
855
|
+
|
|
856
|
+
explicit operator bool() const noexcept { return slots_; }
|
|
857
|
+
std::size_t size() const noexcept { return count_; }
|
|
858
|
+
|
|
859
|
+
void clear() noexcept {
|
|
860
|
+
std::memset(slots_, 0xFF, capacity_ * sizeof(element_t));
|
|
861
|
+
count_ = 0;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
void reset() noexcept {
|
|
865
|
+
if (slots_)
|
|
866
|
+
allocator_t{}.deallocate((byte_t*)slots_, capacity_ * sizeof(element_t));
|
|
867
|
+
slots_ = nullptr;
|
|
868
|
+
capacity_ = 0;
|
|
869
|
+
count_ = 0;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
growing_hash_set_gt(std::size_t capacity) noexcept
|
|
873
|
+
: slots_((element_t*)allocator_t{}.allocate(ceil2(capacity) * sizeof(element_t))),
|
|
874
|
+
capacity_(slots_ ? ceil2(capacity) : 0u), count_(0u) {
|
|
875
|
+
clear();
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
growing_hash_set_gt(growing_hash_set_gt&& other) noexcept {
|
|
879
|
+
slots_ = exchange(other.slots_, nullptr);
|
|
880
|
+
capacity_ = exchange(other.capacity_, 0);
|
|
881
|
+
count_ = exchange(other.count_, 0);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
growing_hash_set_gt& operator=(growing_hash_set_gt&& other) noexcept {
|
|
885
|
+
std::swap(slots_, other.slots_);
|
|
886
|
+
std::swap(capacity_, other.capacity_);
|
|
887
|
+
std::swap(count_, other.count_);
|
|
888
|
+
return *this;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
growing_hash_set_gt(growing_hash_set_gt const&) = delete;
|
|
892
|
+
growing_hash_set_gt& operator=(growing_hash_set_gt const&) = delete;
|
|
893
|
+
|
|
894
|
+
inline bool test(element_t const& elem) const noexcept {
|
|
895
|
+
std::size_t index = hasher_(elem) & (capacity_ - 1);
|
|
896
|
+
while (slots_[index] != default_free_value<element_t>()) {
|
|
897
|
+
if (slots_[index] == elem)
|
|
898
|
+
return true;
|
|
899
|
+
|
|
900
|
+
index = (index + 1) & (capacity_ - 1);
|
|
901
|
+
}
|
|
902
|
+
return false;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
*
|
|
907
|
+
* @return Similar to `bitset_gt`, returns the previous value.
|
|
908
|
+
*/
|
|
909
|
+
inline bool set(element_t const& elem) noexcept {
|
|
910
|
+
std::size_t index = hasher_(elem) & (capacity_ - 1);
|
|
911
|
+
while (slots_[index] != default_free_value<element_t>()) {
|
|
912
|
+
// Already exists
|
|
913
|
+
if (slots_[index] == elem)
|
|
914
|
+
return true;
|
|
915
|
+
|
|
916
|
+
index = (index + 1) & (capacity_ - 1);
|
|
917
|
+
}
|
|
918
|
+
slots_[index] = elem;
|
|
919
|
+
++count_;
|
|
920
|
+
return false;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
bool reserve(std::size_t new_capacity) noexcept {
|
|
924
|
+
new_capacity = (new_capacity * 5u) / 3u;
|
|
925
|
+
if (new_capacity <= capacity_)
|
|
926
|
+
return true;
|
|
927
|
+
|
|
928
|
+
new_capacity = ceil2(new_capacity);
|
|
929
|
+
element_t* new_slots = (element_t*)allocator_t{}.allocate(new_capacity * sizeof(element_t));
|
|
930
|
+
if (!new_slots)
|
|
931
|
+
return false;
|
|
932
|
+
|
|
933
|
+
std::memset(new_slots, 0xFF, new_capacity * sizeof(element_t));
|
|
934
|
+
std::size_t new_count = count_;
|
|
935
|
+
if (count_) {
|
|
936
|
+
for (std::size_t old_index = 0; old_index != capacity_; ++old_index) {
|
|
937
|
+
if (slots_[old_index] == default_free_value<element_t>())
|
|
938
|
+
continue;
|
|
939
|
+
|
|
940
|
+
std::size_t new_index = hasher_(slots_[old_index]) & (new_capacity - 1);
|
|
941
|
+
while (new_slots[new_index] != default_free_value<element_t>())
|
|
942
|
+
new_index = (new_index + 1) & (new_capacity - 1);
|
|
943
|
+
new_slots[new_index] = slots_[old_index];
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
reset();
|
|
948
|
+
slots_ = new_slots;
|
|
949
|
+
capacity_ = new_capacity;
|
|
950
|
+
count_ = new_count;
|
|
951
|
+
return true;
|
|
952
|
+
}
|
|
953
|
+
};
|
|
954
|
+
|
|
809
955
|
/**
|
|
810
956
|
* @brief Basic single-threaded @b ring class, used for all kinds of task queues.
|
|
811
957
|
*/
|
|
@@ -881,7 +1027,9 @@ class ring_gt {
|
|
|
881
1027
|
bool reserve(std::size_t n) noexcept {
|
|
882
1028
|
if (n < size())
|
|
883
1029
|
return false; // prevent data loss
|
|
884
|
-
n
|
|
1030
|
+
if (n <= capacity())
|
|
1031
|
+
return true;
|
|
1032
|
+
n = (std::max<std::size_t>)(ceil2(n), 64u);
|
|
885
1033
|
element_t* elements = allocator_.allocate(n);
|
|
886
1034
|
if (!elements)
|
|
887
1035
|
return false;
|
|
@@ -1000,6 +1148,16 @@ struct index_search_config_t {
|
|
|
1000
1148
|
bool exact = false;
|
|
1001
1149
|
};
|
|
1002
1150
|
|
|
1151
|
+
struct index_cluster_config_t {
|
|
1152
|
+
/// @brief Hyper-parameter controlling the quality of search.
|
|
1153
|
+
/// Defaults to 16 in FAISS and 10 in hnswlib.
|
|
1154
|
+
/// > It is called `ef` in the paper.
|
|
1155
|
+
std::size_t expansion = default_expansion_search();
|
|
1156
|
+
|
|
1157
|
+
/// @brief Optional thread identifier for multi-threaded construction.
|
|
1158
|
+
std::size_t thread = 0;
|
|
1159
|
+
};
|
|
1160
|
+
|
|
1003
1161
|
struct index_copy_config_t {};
|
|
1004
1162
|
|
|
1005
1163
|
struct index_join_config_t {
|
|
@@ -1028,7 +1186,7 @@ using return_type_gt =
|
|
|
1028
1186
|
* @brief An example of what a USearch-compatible ad-hoc filter would look like.
|
|
1029
1187
|
*
|
|
1030
1188
|
* A similar function object can be passed to search queries to further filter entries
|
|
1031
|
-
* on their auxiliary properties, such as some categorical
|
|
1189
|
+
* on their auxiliary properties, such as some categorical keys stored in an external DBMS.
|
|
1032
1190
|
*/
|
|
1033
1191
|
struct dummy_predicate_t {
|
|
1034
1192
|
template <typename member_at> constexpr bool operator()(member_at&&) const noexcept { return true; }
|
|
@@ -1052,7 +1210,7 @@ struct dummy_callback_t {
|
|
|
1052
1210
|
* saving, and loading from disk, or index-level joins.
|
|
1053
1211
|
*/
|
|
1054
1212
|
struct dummy_progress_t {
|
|
1055
|
-
inline void operator()(std::size_t
|
|
1213
|
+
inline void operator()(std::size_t, std::size_t) const noexcept {}
|
|
1056
1214
|
};
|
|
1057
1215
|
|
|
1058
1216
|
/**
|
|
@@ -1078,9 +1236,9 @@ struct dummy_prefetch_t {
|
|
|
1078
1236
|
/**
|
|
1079
1237
|
* @brief An example of what a USearch-compatible executor (thread-pool) should look like.
|
|
1080
1238
|
*
|
|
1081
|
-
* It's expected to have `
|
|
1082
|
-
* an identical `
|
|
1083
|
-
* of tasks, and somehow schedules them between threads; as well as `size()` to
|
|
1239
|
+
* It's expected to have `parallel(callback)` API to schedule one task per thread;
|
|
1240
|
+
* an identical `fixed(count, callback)` and `dynamic(count, callback)` overloads that also accepts
|
|
1241
|
+
* the number of tasks, and somehow schedules them between threads; as well as `size()` to
|
|
1084
1242
|
* determine the number of available threads.
|
|
1085
1243
|
*/
|
|
1086
1244
|
struct dummy_executor_t {
|
|
@@ -1088,25 +1246,32 @@ struct dummy_executor_t {
|
|
|
1088
1246
|
std::size_t size() const noexcept { return 1; }
|
|
1089
1247
|
|
|
1090
1248
|
template <typename thread_aware_function_at>
|
|
1091
|
-
void
|
|
1249
|
+
void fixed(std::size_t tasks, thread_aware_function_at&& thread_aware_function) noexcept {
|
|
1092
1250
|
for (std::size_t task_idx = 0; task_idx != tasks; ++task_idx)
|
|
1093
1251
|
thread_aware_function(0, task_idx);
|
|
1094
1252
|
}
|
|
1095
1253
|
|
|
1096
1254
|
template <typename thread_aware_function_at>
|
|
1097
|
-
void
|
|
1255
|
+
void dynamic(std::size_t tasks, thread_aware_function_at&& thread_aware_function) noexcept {
|
|
1256
|
+
for (std::size_t task_idx = 0; task_idx != tasks; ++task_idx)
|
|
1257
|
+
if (!thread_aware_function(0, task_idx))
|
|
1258
|
+
break;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
template <typename thread_aware_function_at>
|
|
1262
|
+
void parallel(thread_aware_function_at&& thread_aware_function) noexcept {
|
|
1098
1263
|
thread_aware_function(0);
|
|
1099
1264
|
}
|
|
1100
1265
|
};
|
|
1101
1266
|
|
|
1102
1267
|
/**
|
|
1103
|
-
* @brief An example of what a USearch-compatible
|
|
1268
|
+
* @brief An example of what a USearch-compatible key-to-key mapping should look like.
|
|
1104
1269
|
*
|
|
1105
1270
|
* This is particularly helpful for "Semantic Joins", where we map entries of one collection
|
|
1106
1271
|
* to entries of another. In assymetric setups, where A -> B is needed, but B -> A is not,
|
|
1107
1272
|
* this can be passed to minimize memory usage.
|
|
1108
1273
|
*/
|
|
1109
|
-
struct
|
|
1274
|
+
struct dummy_key_to_key_mapping_t {
|
|
1110
1275
|
struct member_ref_t {
|
|
1111
1276
|
template <typename key_at> member_ref_t& operator=(key_at&&) noexcept { return *this; }
|
|
1112
1277
|
};
|
|
@@ -1124,7 +1289,7 @@ template <typename object_at> static constexpr bool is_dummy() {
|
|
|
1124
1289
|
std::is_same<object_t, dummy_progress_t>::value || //
|
|
1125
1290
|
std::is_same<object_t, dummy_prefetch_t>::value || //
|
|
1126
1291
|
std::is_same<object_t, dummy_executor_t>::value || //
|
|
1127
|
-
std::is_same<object_t,
|
|
1292
|
+
std::is_same<object_t, dummy_key_to_key_mapping_t>::value;
|
|
1128
1293
|
}
|
|
1129
1294
|
|
|
1130
1295
|
template <typename, typename at> struct has_reset_gt {
|
|
@@ -1603,7 +1768,9 @@ class index_gt {
|
|
|
1603
1768
|
*/
|
|
1604
1769
|
static constexpr std::size_t node_head_bytes_() { return sizeof(key_t) + sizeof(level_t); }
|
|
1605
1770
|
|
|
1606
|
-
using
|
|
1771
|
+
using nodes_mutexes_t = bitset_gt<dynamic_allocator_t>;
|
|
1772
|
+
|
|
1773
|
+
using visits_hash_set_t = growing_hash_set_gt<compressed_slot_t, hash_gt<compressed_slot_t>, dynamic_allocator_t>;
|
|
1607
1774
|
|
|
1608
1775
|
struct precomputed_constants_t {
|
|
1609
1776
|
double inverse_log_connectivity{};
|
|
@@ -1697,7 +1864,7 @@ class index_gt {
|
|
|
1697
1864
|
struct usearch_align_m context_t {
|
|
1698
1865
|
top_candidates_t top_candidates{};
|
|
1699
1866
|
next_candidates_t next_candidates{};
|
|
1700
|
-
|
|
1867
|
+
visits_hash_set_t visits{};
|
|
1701
1868
|
std::default_random_engine level_generator{};
|
|
1702
1869
|
std::size_t iteration_cycles{};
|
|
1703
1870
|
std::size_t computed_distances_count{};
|
|
@@ -1754,7 +1921,7 @@ class index_gt {
|
|
|
1754
1921
|
buffer_gt<node_t, nodes_allocator_t> nodes_{};
|
|
1755
1922
|
|
|
1756
1923
|
/// @brief Mutex, that limits concurrent access to `nodes_`.
|
|
1757
|
-
mutable
|
|
1924
|
+
mutable nodes_mutexes_t nodes_mutexes_{};
|
|
1758
1925
|
|
|
1759
1926
|
using contexts_allocator_t = typename dynamic_allocator_traits_t::template rebind_alloc<context_t>;
|
|
1760
1927
|
|
|
@@ -1806,7 +1973,7 @@ class index_gt {
|
|
|
1806
1973
|
}
|
|
1807
1974
|
};
|
|
1808
1975
|
|
|
1809
|
-
copy_result_t copy(index_copy_config_t
|
|
1976
|
+
copy_result_t copy(index_copy_config_t config = {}) const noexcept {
|
|
1810
1977
|
copy_result_t result;
|
|
1811
1978
|
index_gt& other = result.index;
|
|
1812
1979
|
other = index_gt(config_, dynamic_allocator_, tape_allocator_);
|
|
@@ -1821,6 +1988,9 @@ class index_gt {
|
|
|
1821
1988
|
other.nodes_count_ = nodes_count_.load();
|
|
1822
1989
|
other.max_level_ = max_level_;
|
|
1823
1990
|
other.entry_slot_ = entry_slot_;
|
|
1991
|
+
|
|
1992
|
+
// This controls nothing for now :)
|
|
1993
|
+
(void)config;
|
|
1824
1994
|
return result;
|
|
1825
1995
|
}
|
|
1826
1996
|
|
|
@@ -1914,18 +2084,12 @@ class index_gt {
|
|
|
1914
2084
|
&& limits.members <= limits_.members)
|
|
1915
2085
|
return true;
|
|
1916
2086
|
|
|
1917
|
-
|
|
2087
|
+
nodes_mutexes_t new_mutexes(limits.members);
|
|
1918
2088
|
buffer_gt<node_t, nodes_allocator_t> new_nodes(limits.members);
|
|
1919
2089
|
buffer_gt<context_t, contexts_allocator_t> new_contexts(limits.threads());
|
|
1920
2090
|
if (!new_nodes || !new_contexts || !new_mutexes)
|
|
1921
2091
|
return false;
|
|
1922
2092
|
|
|
1923
|
-
for (context_t& context : new_contexts) {
|
|
1924
|
-
context.visits = visits_bitset_t(limits.members);
|
|
1925
|
-
if (!context.visits)
|
|
1926
|
-
return false;
|
|
1927
|
-
}
|
|
1928
|
-
|
|
1929
2093
|
// Move the nodes info, and deallocate previous buffers.
|
|
1930
2094
|
if (nodes_)
|
|
1931
2095
|
std::memcpy(new_nodes.data(), nodes_.data(), sizeof(node_t) * size());
|
|
@@ -1961,6 +2125,30 @@ class index_gt {
|
|
|
1961
2125
|
struct match_t {
|
|
1962
2126
|
member_cref_t member;
|
|
1963
2127
|
distance_t distance;
|
|
2128
|
+
|
|
2129
|
+
inline match_t() noexcept : member({nullptr, 0}), distance(std::numeric_limits<distance_t>::max()) {}
|
|
2130
|
+
|
|
2131
|
+
inline match_t(member_cref_t member, distance_t distance) noexcept : member(member), distance(distance) {}
|
|
2132
|
+
|
|
2133
|
+
inline match_t(match_t&& other) noexcept
|
|
2134
|
+
: member({other.member.key.ptr(), other.member.slot}), distance(other.distance) {}
|
|
2135
|
+
|
|
2136
|
+
inline match_t(match_t const& other) noexcept
|
|
2137
|
+
: member({other.member.key.ptr(), other.member.slot}), distance(other.distance) {}
|
|
2138
|
+
|
|
2139
|
+
inline match_t& operator=(match_t const& other) noexcept {
|
|
2140
|
+
member.key.reset(other.member.key.ptr());
|
|
2141
|
+
member.slot = other.member.slot;
|
|
2142
|
+
distance = other.distance;
|
|
2143
|
+
return *this;
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
inline match_t& operator=(match_t&& other) noexcept {
|
|
2147
|
+
member.key.reset(other.member.key.ptr());
|
|
2148
|
+
member.slot = other.member.slot;
|
|
2149
|
+
distance = other.distance;
|
|
2150
|
+
return *this;
|
|
2151
|
+
}
|
|
1964
2152
|
};
|
|
1965
2153
|
|
|
1966
2154
|
class search_result_t {
|
|
@@ -2046,6 +2234,19 @@ class index_gt {
|
|
|
2046
2234
|
}
|
|
2047
2235
|
};
|
|
2048
2236
|
|
|
2237
|
+
struct cluster_result_t {
|
|
2238
|
+
error_t error{};
|
|
2239
|
+
std::size_t visited_members{};
|
|
2240
|
+
std::size_t computed_distances{};
|
|
2241
|
+
match_t cluster{};
|
|
2242
|
+
|
|
2243
|
+
explicit operator bool() const noexcept { return !error; }
|
|
2244
|
+
cluster_result_t failed(error_t message) noexcept {
|
|
2245
|
+
error = std::move(message);
|
|
2246
|
+
return std::move(*this);
|
|
2247
|
+
}
|
|
2248
|
+
};
|
|
2249
|
+
|
|
2049
2250
|
/**
|
|
2050
2251
|
* @brief Inserts a new entry into the index. Thread-safe. Supports @b heterogeneous lookups.
|
|
2051
2252
|
* Expects needed capacity to be reserved ahead of time: `size() < capacity()`.
|
|
@@ -2240,7 +2441,7 @@ class index_gt {
|
|
|
2240
2441
|
* @param[in] wanted The upper bound for the number of results to return.
|
|
2241
2442
|
* @param[in] config Configuration options for this specific operation.
|
|
2242
2443
|
* @param[in] predicate Optional filtering predicate for `member_cref_t`.
|
|
2243
|
-
* @return Smart object referencing temporary memory. Valid until next `search()` or `
|
|
2444
|
+
* @return Smart object referencing temporary memory. Valid until next `search()`, `add()`, or `cluster()`.
|
|
2244
2445
|
*/
|
|
2245
2446
|
template < //
|
|
2246
2447
|
typename value_at, //
|
|
@@ -2295,27 +2496,75 @@ class index_gt {
|
|
|
2295
2496
|
return result;
|
|
2296
2497
|
}
|
|
2297
2498
|
|
|
2499
|
+
/**
|
|
2500
|
+
* @brief Identifies the closest cluster to the gived ::query. Thread-safe.
|
|
2501
|
+
*
|
|
2502
|
+
* @param[in] query Content that will be compared against other entries in the index.
|
|
2503
|
+
* @param[in] level The index level to target. Higher means lower resolution.
|
|
2504
|
+
* @param[in] config Configuration options for this specific operation.
|
|
2505
|
+
* @param[in] predicate Optional filtering predicate for `member_cref_t`.
|
|
2506
|
+
* @return Smart object referencing temporary memory. Valid until next `search()`, `add()`, or `cluster()`.
|
|
2507
|
+
*/
|
|
2508
|
+
template < //
|
|
2509
|
+
typename value_at, //
|
|
2510
|
+
typename metric_at, //
|
|
2511
|
+
typename predicate_at = dummy_predicate_t, //
|
|
2512
|
+
typename prefetch_at = dummy_prefetch_t //
|
|
2513
|
+
>
|
|
2514
|
+
cluster_result_t cluster( //
|
|
2515
|
+
value_at&& query, //
|
|
2516
|
+
std::size_t level, //
|
|
2517
|
+
metric_at&& metric, //
|
|
2518
|
+
index_cluster_config_t config = {}, //
|
|
2519
|
+
predicate_at&& predicate = predicate_at{}, //
|
|
2520
|
+
prefetch_at&& prefetch = prefetch_at{}) const noexcept {
|
|
2521
|
+
|
|
2522
|
+
context_t& context = contexts_[config.thread];
|
|
2523
|
+
cluster_result_t result;
|
|
2524
|
+
if (!nodes_count_)
|
|
2525
|
+
return result.failed("No clusters to identify");
|
|
2526
|
+
|
|
2527
|
+
// Go down the level, tracking only the closest match
|
|
2528
|
+
result.computed_distances = context.computed_distances_count;
|
|
2529
|
+
result.visited_members = context.iteration_cycles;
|
|
2530
|
+
|
|
2531
|
+
next_candidates_t& next = context.next_candidates;
|
|
2532
|
+
std::size_t expansion = config.expansion;
|
|
2533
|
+
if (!next.reserve(expansion))
|
|
2534
|
+
return result.failed("Out of memory!");
|
|
2535
|
+
|
|
2536
|
+
result.cluster.member =
|
|
2537
|
+
at(search_for_one_(query, metric, prefetch, entry_slot_, max_level_, level - 1, context));
|
|
2538
|
+
result.cluster.distance = context.measure(query, result.cluster.member, metric);
|
|
2539
|
+
|
|
2540
|
+
// Normalize stats
|
|
2541
|
+
result.computed_distances = context.computed_distances_count - result.computed_distances;
|
|
2542
|
+
result.visited_members = context.iteration_cycles - result.visited_members;
|
|
2543
|
+
return result;
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2298
2546
|
#pragma endregion
|
|
2299
2547
|
|
|
2300
2548
|
#pragma region Metadata
|
|
2301
2549
|
|
|
2302
2550
|
struct stats_t {
|
|
2303
|
-
std::size_t nodes;
|
|
2304
|
-
std::size_t edges;
|
|
2305
|
-
std::size_t max_edges;
|
|
2306
|
-
std::size_t allocated_bytes;
|
|
2551
|
+
std::size_t nodes{};
|
|
2552
|
+
std::size_t edges{};
|
|
2553
|
+
std::size_t max_edges{};
|
|
2554
|
+
std::size_t allocated_bytes{};
|
|
2307
2555
|
};
|
|
2308
2556
|
|
|
2309
2557
|
stats_t stats() const noexcept {
|
|
2310
2558
|
stats_t result{};
|
|
2311
|
-
|
|
2312
|
-
for (std::size_t i = 0; i !=
|
|
2559
|
+
|
|
2560
|
+
for (std::size_t i = 0; i != size(); ++i) {
|
|
2313
2561
|
node_t node = node_at_(i);
|
|
2314
2562
|
std::size_t max_edges = node.level() * config_.connectivity + config_.connectivity_base;
|
|
2315
2563
|
std::size_t edges = 0;
|
|
2316
2564
|
for (level_t level = 0; level <= node.level(); ++level)
|
|
2317
2565
|
edges += neighbors_(node, level).size();
|
|
2318
2566
|
|
|
2567
|
+
++result.nodes;
|
|
2319
2568
|
result.allocated_bytes += node_bytes_(node).size();
|
|
2320
2569
|
result.edges += edges;
|
|
2321
2570
|
result.max_edges += max_edges;
|
|
@@ -2325,14 +2574,14 @@ class index_gt {
|
|
|
2325
2574
|
|
|
2326
2575
|
stats_t stats(std::size_t level) const noexcept {
|
|
2327
2576
|
stats_t result{};
|
|
2328
|
-
result.nodes = size();
|
|
2329
2577
|
|
|
2330
2578
|
std::size_t neighbors_bytes = !level ? pre_.neighbors_base_bytes : pre_.neighbors_bytes;
|
|
2331
|
-
for (std::size_t i = 0; i !=
|
|
2579
|
+
for (std::size_t i = 0; i != size(); ++i) {
|
|
2332
2580
|
node_t node = node_at_(i);
|
|
2333
2581
|
if (static_cast<std::size_t>(node.level()) < level)
|
|
2334
2582
|
continue;
|
|
2335
2583
|
|
|
2584
|
+
++result.nodes;
|
|
2336
2585
|
result.edges += neighbors_(node, level).size();
|
|
2337
2586
|
result.allocated_bytes += node_head_bytes_() + neighbors_bytes;
|
|
2338
2587
|
}
|
|
@@ -2432,7 +2681,7 @@ class index_gt {
|
|
|
2432
2681
|
std::size_t stream_length() const noexcept {
|
|
2433
2682
|
std::size_t neighbors_length = 0;
|
|
2434
2683
|
for (std::size_t i = 0; i != size(); ++i)
|
|
2435
|
-
neighbors_length += node_bytes_(node_at_(i).level());
|
|
2684
|
+
neighbors_length += node_bytes_(node_at_(i).level()) + sizeof(level_t);
|
|
2436
2685
|
return sizeof(index_serialized_header_t) + neighbors_length;
|
|
2437
2686
|
}
|
|
2438
2687
|
|
|
@@ -2457,6 +2706,11 @@ class index_gt {
|
|
|
2457
2706
|
if (!result)
|
|
2458
2707
|
return result;
|
|
2459
2708
|
|
|
2709
|
+
if (!header.size) {
|
|
2710
|
+
reset();
|
|
2711
|
+
return result;
|
|
2712
|
+
}
|
|
2713
|
+
|
|
2460
2714
|
// Allocate some dynamic memory to read all the levels
|
|
2461
2715
|
using levels_allocator_t = typename dynamic_allocator_traits_t::template rebind_alloc<level_t>;
|
|
2462
2716
|
buffer_gt<level_t, levels_allocator_t> levels(header.size);
|
|
@@ -2515,6 +2769,11 @@ class index_gt {
|
|
|
2515
2769
|
return result.failed("File is corrupted and lacks a header");
|
|
2516
2770
|
std::memcpy(&header, file.data() + offset, sizeof(header));
|
|
2517
2771
|
|
|
2772
|
+
if (!header.size) {
|
|
2773
|
+
reset();
|
|
2774
|
+
return result;
|
|
2775
|
+
}
|
|
2776
|
+
|
|
2518
2777
|
// Precompute offsets of every node, but before that we need to update the configs
|
|
2519
2778
|
// This could have been done with `std::exclusive_scan`, but it's only available from C++17.
|
|
2520
2779
|
using offsets_allocator_t = typename dynamic_allocator_traits_t::template rebind_alloc<std::size_t>;
|
|
@@ -2574,10 +2833,10 @@ class index_gt {
|
|
|
2574
2833
|
* @param[in] executor Thread-pool to execute the job in parallel.
|
|
2575
2834
|
* @param[in] progress Callback to report the execution progress.
|
|
2576
2835
|
*/
|
|
2577
|
-
template <typename values_at, typename metric_at,
|
|
2578
|
-
typename slot_transition_at =
|
|
2579
|
-
typename executor_at = dummy_executor_t,
|
|
2580
|
-
typename progress_at = dummy_progress_t,
|
|
2836
|
+
template <typename values_at, typename metric_at, //
|
|
2837
|
+
typename slot_transition_at = dummy_key_to_key_mapping_t, //
|
|
2838
|
+
typename executor_at = dummy_executor_t, //
|
|
2839
|
+
typename progress_at = dummy_progress_t, //
|
|
2581
2840
|
typename prefetch_at = dummy_prefetch_t>
|
|
2582
2841
|
void compact( //
|
|
2583
2842
|
values_at&& values, //
|
|
@@ -2601,7 +2860,7 @@ class index_gt {
|
|
|
2601
2860
|
buffer_gt<slot_level_t, slot_level_allocator_t> slots_and_levels(size());
|
|
2602
2861
|
|
|
2603
2862
|
// For every bottom level node, determine its parent cluster
|
|
2604
|
-
executor.
|
|
2863
|
+
executor.fixed(slots_and_levels.size(), [&](std::size_t thread_idx, std::size_t old_slot) {
|
|
2605
2864
|
context_t& context = contexts_[thread_idx];
|
|
2606
2865
|
std::size_t cluster = search_for_one_( //
|
|
2607
2866
|
values[citerator_at(old_slot)], //
|
|
@@ -2680,7 +2939,7 @@ class index_gt {
|
|
|
2680
2939
|
|
|
2681
2940
|
// Erase all the incoming links
|
|
2682
2941
|
std::size_t nodes_count = size();
|
|
2683
|
-
executor.
|
|
2942
|
+
executor.fixed(nodes_count, [&](std::size_t, std::size_t node_idx) {
|
|
2684
2943
|
node_t node = node_at_(node_idx);
|
|
2685
2944
|
for (level_t level = 0; level <= node.level(); ++level) {
|
|
2686
2945
|
neighbors_ref_t neighbors = neighbors_(node, level);
|
|
@@ -2764,9 +3023,9 @@ class index_gt {
|
|
|
2764
3023
|
}
|
|
2765
3024
|
|
|
2766
3025
|
struct node_lock_t {
|
|
2767
|
-
|
|
3026
|
+
nodes_mutexes_t& mutexes;
|
|
2768
3027
|
std::size_t slot;
|
|
2769
|
-
inline ~node_lock_t() noexcept {
|
|
3028
|
+
inline ~node_lock_t() noexcept { mutexes.atomic_reset(slot); }
|
|
2770
3029
|
};
|
|
2771
3030
|
|
|
2772
3031
|
inline node_lock_t node_lock_(std::size_t slot) const noexcept {
|
|
@@ -2876,10 +3135,12 @@ class index_gt {
|
|
|
2876
3135
|
|
|
2877
3136
|
index_gt const& index_;
|
|
2878
3137
|
neighbors_ref_t neighbors_;
|
|
2879
|
-
|
|
3138
|
+
visits_hash_set_t& visits_;
|
|
2880
3139
|
std::size_t current_;
|
|
2881
3140
|
|
|
2882
3141
|
candidates_iterator_t& skip_missing() noexcept {
|
|
3142
|
+
if (!visits_.size())
|
|
3143
|
+
return *this;
|
|
2883
3144
|
while (current_ != neighbors_.size()) {
|
|
2884
3145
|
compressed_slot_t neighbor_slot = neighbors_[current_];
|
|
2885
3146
|
if (visits_.test(neighbor_slot))
|
|
@@ -2899,7 +3160,7 @@ class index_gt {
|
|
|
2899
3160
|
using reference = misaligned_ref_gt<element_t>;
|
|
2900
3161
|
|
|
2901
3162
|
reference operator*() const noexcept { return slot(); }
|
|
2902
|
-
candidates_iterator_t(index_gt const& index, neighbors_ref_t neighbors,
|
|
3163
|
+
candidates_iterator_t(index_gt const& index, neighbors_ref_t neighbors, visits_hash_set_t& visits,
|
|
2903
3164
|
std::size_t progress) noexcept
|
|
2904
3165
|
: index_(index), neighbors_(neighbors), visits_(visits), current_(progress) {}
|
|
2905
3166
|
candidates_iterator_t operator++(int) noexcept {
|
|
@@ -2922,7 +3183,7 @@ class index_gt {
|
|
|
2922
3183
|
struct candidates_range_t {
|
|
2923
3184
|
index_gt const& index;
|
|
2924
3185
|
neighbors_ref_t neighbors;
|
|
2925
|
-
|
|
3186
|
+
visits_hash_set_t& visits;
|
|
2926
3187
|
|
|
2927
3188
|
candidates_iterator_t begin() const noexcept {
|
|
2928
3189
|
return candidates_iterator_t{index, neighbors, visits, 0}.skip_missing();
|
|
@@ -2935,7 +3196,7 @@ class index_gt {
|
|
|
2935
3196
|
value_at&& query, metric_at&& metric, prefetch_at&& prefetch, //
|
|
2936
3197
|
std::size_t closest_slot, level_t begin_level, level_t end_level, context_t& context) const noexcept {
|
|
2937
3198
|
|
|
2938
|
-
|
|
3199
|
+
visits_hash_set_t& visits = context.visits;
|
|
2939
3200
|
visits.clear();
|
|
2940
3201
|
|
|
2941
3202
|
// Optional prefetching
|
|
@@ -2982,13 +3243,15 @@ class index_gt {
|
|
|
2982
3243
|
std::size_t start_slot, std::size_t new_slot, level_t level, std::size_t top_limit,
|
|
2983
3244
|
context_t& context) noexcept {
|
|
2984
3245
|
|
|
2985
|
-
|
|
3246
|
+
visits_hash_set_t& visits = context.visits;
|
|
2986
3247
|
next_candidates_t& next = context.next_candidates; // pop min, push
|
|
2987
3248
|
top_candidates_t& top = context.top_candidates; // pop max, push
|
|
2988
3249
|
|
|
2989
3250
|
visits.clear();
|
|
2990
3251
|
next.clear();
|
|
2991
3252
|
top.clear();
|
|
3253
|
+
if (!visits.reserve(config_.connectivity_base + 1u))
|
|
3254
|
+
return false;
|
|
2992
3255
|
|
|
2993
3256
|
// Optional prefetching
|
|
2994
3257
|
if (!std::is_same<prefetch_at, dummy_prefetch_t>::value)
|
|
@@ -3021,6 +3284,10 @@ class index_gt {
|
|
|
3021
3284
|
prefetch(missing_candidates.begin(), missing_candidates.end());
|
|
3022
3285
|
}
|
|
3023
3286
|
|
|
3287
|
+
// Assume the worst-case when reserving memory
|
|
3288
|
+
if (!visits.reserve(visits.size() + candidate_neighbors.size()))
|
|
3289
|
+
return false;
|
|
3290
|
+
|
|
3024
3291
|
for (compressed_slot_t successor_slot : candidate_neighbors) {
|
|
3025
3292
|
if (visits.set(successor_slot))
|
|
3026
3293
|
continue;
|
|
@@ -3049,7 +3316,7 @@ class index_gt {
|
|
|
3049
3316
|
value_at&& query, metric_at&& metric, predicate_at&& predicate, prefetch_at&& prefetch, //
|
|
3050
3317
|
std::size_t start_slot, std::size_t expansion, context_t& context) const noexcept {
|
|
3051
3318
|
|
|
3052
|
-
|
|
3319
|
+
visits_hash_set_t& visits = context.visits;
|
|
3053
3320
|
next_candidates_t& next = context.next_candidates; // pop min, push
|
|
3054
3321
|
top_candidates_t& top = context.top_candidates; // pop max, push
|
|
3055
3322
|
std::size_t const top_limit = expansion;
|
|
@@ -3057,6 +3324,8 @@ class index_gt {
|
|
|
3057
3324
|
visits.clear();
|
|
3058
3325
|
next.clear();
|
|
3059
3326
|
top.clear();
|
|
3327
|
+
if (!visits.reserve(config_.connectivity_base + 1u))
|
|
3328
|
+
return false;
|
|
3060
3329
|
|
|
3061
3330
|
// Optional prefetching
|
|
3062
3331
|
if (!std::is_same<prefetch_at, dummy_prefetch_t>::value)
|
|
@@ -3084,6 +3353,10 @@ class index_gt {
|
|
|
3084
3353
|
prefetch(missing_candidates.begin(), missing_candidates.end());
|
|
3085
3354
|
}
|
|
3086
3355
|
|
|
3356
|
+
// Assume the worst-case when reserving memory
|
|
3357
|
+
if (!visits.reserve(visits.size() + candidate_neighbors.size()))
|
|
3358
|
+
return false;
|
|
3359
|
+
|
|
3087
3360
|
for (compressed_slot_t successor_slot : candidate_neighbors) {
|
|
3088
3361
|
if (visits.set(successor_slot))
|
|
3089
3362
|
continue;
|
|
@@ -3205,10 +3478,10 @@ template < //
|
|
|
3205
3478
|
typename men_metric_at, //
|
|
3206
3479
|
typename women_metric_at, //
|
|
3207
3480
|
|
|
3208
|
-
typename man_to_woman_at =
|
|
3209
|
-
typename woman_to_man_at =
|
|
3210
|
-
typename executor_at = dummy_executor_t,
|
|
3211
|
-
typename progress_at = dummy_progress_t
|
|
3481
|
+
typename man_to_woman_at = dummy_key_to_key_mapping_t, //
|
|
3482
|
+
typename woman_to_man_at = dummy_key_to_key_mapping_t, //
|
|
3483
|
+
typename executor_at = dummy_executor_t, //
|
|
3484
|
+
typename progress_at = dummy_progress_t //
|
|
3212
3485
|
>
|
|
3213
3486
|
static join_result_t join( //
|
|
3214
3487
|
men_at const& men, //
|
|
@@ -3249,6 +3522,7 @@ static join_result_t join( //
|
|
|
3249
3522
|
config.max_proposals = (std::min)(men.size(), config.max_proposals);
|
|
3250
3523
|
|
|
3251
3524
|
using distance_t = typename men_at::distance_t;
|
|
3525
|
+
using dynamic_allocator_t = typename men_at::dynamic_allocator_t;
|
|
3252
3526
|
using dynamic_allocator_traits_t = typename men_at::dynamic_allocator_traits_t;
|
|
3253
3527
|
using man_key_t = typename men_at::key_t;
|
|
3254
3528
|
using woman_key_t = typename women_at::key_t;
|
|
@@ -3280,7 +3554,7 @@ static join_result_t join( //
|
|
|
3280
3554
|
std::memset(proposal_counts.data(), 0, sizeof(proposals_count_t) * men.size());
|
|
3281
3555
|
|
|
3282
3556
|
// Define locks, to limit concurrent accesses to `man_to_woman_slots` and `woman_to_man_slots`.
|
|
3283
|
-
|
|
3557
|
+
bitset_t men_locks(men.size()), women_locks(women.size());
|
|
3284
3558
|
if (!men_locks || !women_locks)
|
|
3285
3559
|
return result.failed("Can't allocate locks");
|
|
3286
3560
|
|
|
@@ -3288,9 +3562,10 @@ static join_result_t join( //
|
|
|
3288
3562
|
std::atomic<std::size_t> engagements{0};
|
|
3289
3563
|
std::atomic<std::size_t> computed_distances{0};
|
|
3290
3564
|
std::atomic<std::size_t> visited_members{0};
|
|
3565
|
+
std::atomic<char const*> atomic_error{nullptr};
|
|
3291
3566
|
|
|
3292
3567
|
// Concurrently process all the men
|
|
3293
|
-
executor.
|
|
3568
|
+
executor.parallel([&](std::size_t thread_idx) {
|
|
3294
3569
|
index_search_config_t search_config;
|
|
3295
3570
|
search_config.expansion = config.expansion;
|
|
3296
3571
|
search_config.exact = config.exact;
|
|
@@ -3298,7 +3573,7 @@ static join_result_t join( //
|
|
|
3298
3573
|
compressed_slot_t free_man_slot;
|
|
3299
3574
|
|
|
3300
3575
|
// While there exist a free man who still has a woman to propose to.
|
|
3301
|
-
while (
|
|
3576
|
+
while (!atomic_error.load(std::memory_order_relaxed)) {
|
|
3302
3577
|
std::size_t passed_rounds = 0;
|
|
3303
3578
|
std::size_t total_rounds = 0;
|
|
3304
3579
|
{
|
|
@@ -3323,7 +3598,8 @@ static join_result_t join( //
|
|
|
3323
3598
|
visited_members += candidates.visited_members;
|
|
3324
3599
|
computed_distances += candidates.computed_distances;
|
|
3325
3600
|
if (!candidates) {
|
|
3326
|
-
|
|
3601
|
+
atomic_error = candidates.error.release();
|
|
3602
|
+
break;
|
|
3327
3603
|
}
|
|
3328
3604
|
|
|
3329
3605
|
auto match = candidates.back();
|
|
@@ -3366,6 +3642,9 @@ static join_result_t join( //
|
|
|
3366
3642
|
}
|
|
3367
3643
|
});
|
|
3368
3644
|
|
|
3645
|
+
if (atomic_error)
|
|
3646
|
+
return result.failed(atomic_error.load());
|
|
3647
|
+
|
|
3369
3648
|
// Export the "slots" into keys:
|
|
3370
3649
|
std::size_t intersection_size = 0;
|
|
3371
3650
|
for (std::size_t man_slot = 0; man_slot != men.size(); ++man_slot) {
|