usearch 2.25.0 → 2.25.3
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/README.md +43 -47
- package/c/README.md +1 -1
- package/c/lib.cpp +28 -13
- package/c/test.c +41 -0
- package/cpp/test.cpp +99 -0
- package/include/usearch/index.hpp +338 -57
- package/include/usearch/index_dense.hpp +155 -50
- package/include/usearch/index_plugins.hpp +94 -35
- package/javascript/README.md +1 -1
- package/javascript/lib.cpp +4 -2
- package/javascript/usearch.test.js +117 -94
- package/numkong/CITATION.cff +1 -1
- package/numkong/Cargo.toml +1 -1
- package/numkong/bench/bench_cross_amx.cpp +12 -0
- package/numkong/bench/bench_mesh.cpp +12 -0
- package/numkong/bench/bench_spatial.cpp +0 -3
- package/numkong/binding.gyp +18 -0
- package/numkong/build.rs +1 -0
- package/numkong/c/dispatch_e5m2.c +23 -3
- package/numkong/include/numkong/capabilities.h +1 -1
- package/numkong/include/numkong/cast/README.md +3 -0
- package/numkong/include/numkong/cast/haswell.h +28 -64
- package/numkong/include/numkong/cast/serial.h +17 -0
- package/numkong/include/numkong/cast/skylake.h +67 -52
- package/numkong/include/numkong/cast.h +1 -0
- package/numkong/include/numkong/dot/README.md +1 -0
- package/numkong/include/numkong/dot/haswell.h +92 -13
- package/numkong/include/numkong/dot/serial.h +15 -0
- package/numkong/include/numkong/dot/skylake.h +61 -14
- package/numkong/include/numkong/dots/README.md +2 -0
- package/numkong/include/numkong/dots/graniteamx.h +434 -0
- package/numkong/include/numkong/dots/haswell.h +28 -28
- package/numkong/include/numkong/dots/sapphireamx.h +1 -1
- package/numkong/include/numkong/dots/serial.h +23 -8
- package/numkong/include/numkong/dots/skylake.h +28 -23
- package/numkong/include/numkong/dots.h +12 -0
- package/numkong/include/numkong/each/serial.h +18 -1
- package/numkong/include/numkong/geospatial/serial.h +14 -3
- package/numkong/include/numkong/maxsim/serial.h +15 -0
- package/numkong/include/numkong/mesh/README.md +50 -44
- package/numkong/include/numkong/mesh/genoa.h +462 -0
- package/numkong/include/numkong/mesh/haswell.h +806 -933
- package/numkong/include/numkong/mesh/neon.h +871 -943
- package/numkong/include/numkong/mesh/neonbfdot.h +382 -522
- package/numkong/include/numkong/mesh/neonfhm.h +676 -0
- package/numkong/include/numkong/mesh/rvv.h +404 -319
- package/numkong/include/numkong/mesh/serial.h +204 -162
- package/numkong/include/numkong/mesh/skylake.h +1029 -1585
- package/numkong/include/numkong/mesh/v128relaxed.h +403 -377
- package/numkong/include/numkong/mesh.h +38 -0
- package/numkong/include/numkong/reduce/serial.h +15 -1
- package/numkong/include/numkong/sparse/serial.h +17 -2
- package/numkong/include/numkong/spatial/genoa.h +0 -68
- package/numkong/include/numkong/spatial/haswell.h +98 -56
- package/numkong/include/numkong/spatial/serial.h +15 -0
- package/numkong/include/numkong/spatial/skylake.h +114 -54
- package/numkong/include/numkong/spatial.h +0 -12
- package/numkong/include/numkong/spatials/graniteamx.h +128 -0
- package/numkong/include/numkong/spatials/serial.h +18 -1
- package/numkong/include/numkong/spatials/skylake.h +2 -2
- package/numkong/include/numkong/spatials.h +17 -0
- package/numkong/include/numkong/tensor.hpp +107 -23
- package/numkong/javascript/@numkong-darwin-arm64/package.json +1 -1
- package/numkong/javascript/@numkong-darwin-x64/package.json +1 -1
- package/numkong/javascript/@numkong-linux-arm64/package.json +1 -1
- package/numkong/javascript/@numkong-linux-x64/package.json +1 -1
- package/numkong/javascript/@numkong-win32-arm64/package.json +1 -1
- package/numkong/javascript/@numkong-win32-x64/package.json +1 -1
- package/numkong/javascript/numkong.c +3 -2
- package/numkong/package.json +7 -7
- package/numkong/test/test.cu +841 -0
- package/numkong/test/test_cross_amx.cpp +13 -0
- package/numkong/test/test_dlpack.py +492 -0
- package/numkong/test/test_mesh.cpp +12 -0
- package/numkong/test/test_spatial.cpp +0 -3
- package/numkong/test/test_tensor.cpp +87 -27
- package/package.json +1 -1
- package/prebuilds/darwin-arm64+x64/usearch.node +0 -0
- package/prebuilds/linux-arm64/usearch.node +0 -0
- package/prebuilds/linux-x64/usearch.node +0 -0
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
#define USEARCH_VERSION_MAJOR 2
|
|
11
11
|
#define USEARCH_VERSION_MINOR 25
|
|
12
|
-
#define USEARCH_VERSION_PATCH
|
|
12
|
+
#define USEARCH_VERSION_PATCH 3
|
|
13
13
|
|
|
14
14
|
// Inferring C++ version
|
|
15
15
|
// https://stackoverflow.com/a/61552074
|
|
@@ -176,6 +176,40 @@ namespace usearch {
|
|
|
176
176
|
|
|
177
177
|
using byte_t = char;
|
|
178
178
|
|
|
179
|
+
struct checked_size_result_t {
|
|
180
|
+
std::size_t value;
|
|
181
|
+
bool overflow;
|
|
182
|
+
constexpr checked_size_result_t(std::size_t value = 0, bool overflow = false) noexcept
|
|
183
|
+
: value(value), overflow(overflow) {}
|
|
184
|
+
constexpr explicit operator bool() const noexcept { return !overflow; }
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
constexpr checked_size_result_t checked_size_overflow() noexcept { return {0, true}; }
|
|
188
|
+
|
|
189
|
+
constexpr checked_size_result_t checked_size_from_u64(std::uint64_t value) noexcept {
|
|
190
|
+
return value > static_cast<std::uint64_t>((std::numeric_limits<std::size_t>::max)())
|
|
191
|
+
? checked_size_overflow()
|
|
192
|
+
: checked_size_result_t{static_cast<std::size_t>(value), false};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
constexpr checked_size_result_t checked_add(std::size_t a, std::size_t b) noexcept {
|
|
196
|
+
return (std::numeric_limits<std::size_t>::max)() - a < b ? checked_size_overflow()
|
|
197
|
+
: checked_size_result_t{a + b, false};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
constexpr checked_size_result_t checked_mul(std::size_t a, std::size_t b) noexcept {
|
|
201
|
+
return a && b > (std::numeric_limits<std::size_t>::max)() / a ? checked_size_overflow()
|
|
202
|
+
: checked_size_result_t{a * b, false};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
constexpr checked_size_result_t checked_mul_add_(checked_size_result_t product, std::size_t c) noexcept {
|
|
206
|
+
return product ? checked_add(product.value, c) : product;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
constexpr checked_size_result_t checked_mul_add(std::size_t a, std::size_t b, std::size_t c) noexcept {
|
|
210
|
+
return checked_mul_add_(checked_mul(a, b), c);
|
|
211
|
+
}
|
|
212
|
+
|
|
179
213
|
template <std::size_t multiple_ak> std::size_t divide_round_up(std::size_t num) noexcept {
|
|
180
214
|
return (num + multiple_ak - 1) / multiple_ak;
|
|
181
215
|
}
|
|
@@ -184,6 +218,21 @@ inline std::size_t divide_round_up(std::size_t num, std::size_t denominator) noe
|
|
|
184
218
|
return (num + denominator - 1) / denominator;
|
|
185
219
|
}
|
|
186
220
|
|
|
221
|
+
constexpr checked_size_result_t checked_divide_round_up(std::size_t num, std::size_t denominator) noexcept {
|
|
222
|
+
return !denominator ? checked_size_overflow()
|
|
223
|
+
: (std::numeric_limits<std::size_t>::max)() - num < denominator - 1
|
|
224
|
+
? checked_size_overflow()
|
|
225
|
+
: checked_size_result_t{(num + denominator - 1) / denominator, false};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
constexpr checked_size_result_t checked_round_up_(checked_size_result_t quotient, std::size_t multiple) noexcept {
|
|
229
|
+
return quotient ? checked_mul(quotient.value, multiple) : quotient;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
constexpr checked_size_result_t checked_round_up(std::size_t num, std::size_t multiple) noexcept {
|
|
233
|
+
return checked_round_up_(checked_divide_round_up(num, multiple), multiple);
|
|
234
|
+
}
|
|
235
|
+
|
|
187
236
|
inline std::size_t ceil2(std::size_t v) noexcept {
|
|
188
237
|
v--;
|
|
189
238
|
v |= v >> 1;
|
|
@@ -198,6 +247,14 @@ inline std::size_t ceil2(std::size_t v) noexcept {
|
|
|
198
247
|
return v;
|
|
199
248
|
}
|
|
200
249
|
|
|
250
|
+
inline checked_size_result_t checked_ceil2(std::size_t v) noexcept {
|
|
251
|
+
if (!v)
|
|
252
|
+
return checked_size_result_t{0, false};
|
|
253
|
+
if (v > (std::size_t{1} << ((sizeof(std::size_t) * CHAR_BIT) - 1)))
|
|
254
|
+
return checked_size_overflow();
|
|
255
|
+
return checked_size_result_t{ceil2(v), false};
|
|
256
|
+
}
|
|
257
|
+
|
|
201
258
|
/// @brief Simply dereferencing misaligned pointers can be dangerous.
|
|
202
259
|
template <typename at> void misaligned_store(void* ptr, at v) noexcept {
|
|
203
260
|
static_assert(!std::is_reference<at>::value, "Can't store a reference");
|
|
@@ -525,9 +582,12 @@ template <typename allocator_at = std::allocator<byte_t>> class bitset_gt {
|
|
|
525
582
|
count_ = 0;
|
|
526
583
|
}
|
|
527
584
|
|
|
528
|
-
bitset_gt(std::size_t capacity) noexcept
|
|
529
|
-
|
|
530
|
-
|
|
585
|
+
bitset_gt(std::size_t capacity) noexcept {
|
|
586
|
+
checked_size_result_t slots_count = checked_divide_round_up(capacity, bits_per_slot());
|
|
587
|
+
checked_size_result_t bytes =
|
|
588
|
+
slots_count ? checked_mul(slots_count.value, sizeof(compressed_slot_t)) : slots_count;
|
|
589
|
+
slots_ = bytes ? (compressed_slot_t*)allocator_t{}.allocate(bytes.value) : nullptr;
|
|
590
|
+
count_ = slots_ ? slots_count.value : 0u;
|
|
531
591
|
clear();
|
|
532
592
|
}
|
|
533
593
|
|
|
@@ -654,10 +714,19 @@ class striped_locks_gt {
|
|
|
654
714
|
}
|
|
655
715
|
|
|
656
716
|
striped_locks_gt(std::size_t threads, std::size_t connectivity) noexcept {
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
717
|
+
checked_size_result_t desired = checked_mul(threads, connectivity);
|
|
718
|
+
desired = desired ? checked_mul(desired.value, std::size_t{4}) : desired;
|
|
719
|
+
if (!desired) {
|
|
720
|
+
shift_ = 64;
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
checked_size_result_t count = checked_ceil2((std::max<std::size_t>)(desired.value, 256));
|
|
725
|
+
if (!count) {
|
|
726
|
+
shift_ = 64;
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
count_ = count.value;
|
|
661
730
|
shift_ = 64;
|
|
662
731
|
for (std::size_t n = count_; n > 1; n >>= 1)
|
|
663
732
|
shift_--;
|
|
@@ -665,7 +734,13 @@ class striped_locks_gt {
|
|
|
665
734
|
// `cache_line_ak`-aligned address inside the allocation, regardless of
|
|
666
735
|
// what the underlying allocator returns.
|
|
667
736
|
constexpr std::size_t alignment_k = alignof(padded_lock_t);
|
|
668
|
-
|
|
737
|
+
checked_size_result_t raw_bytes = checked_mul_add(count_, sizeof(padded_lock_t), alignment_k);
|
|
738
|
+
if (!raw_bytes) {
|
|
739
|
+
count_ = 0;
|
|
740
|
+
shift_ = 64;
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
raw_bytes_ = raw_bytes.value;
|
|
669
744
|
raw_ = allocator_t{}.allocate(raw_bytes_);
|
|
670
745
|
if (!raw_) {
|
|
671
746
|
raw_bytes_ = 0;
|
|
@@ -850,10 +925,14 @@ class max_heap_gt {
|
|
|
850
925
|
if (new_capacity <= capacity_)
|
|
851
926
|
return true;
|
|
852
927
|
|
|
853
|
-
|
|
854
|
-
if (
|
|
928
|
+
checked_size_result_t rounded_capacity = checked_ceil2(new_capacity);
|
|
929
|
+
if (!rounded_capacity)
|
|
930
|
+
return false;
|
|
931
|
+
checked_size_result_t doubled_capacity = checked_mul(capacity_, std::size_t{2});
|
|
932
|
+
if (!doubled_capacity)
|
|
855
933
|
return false;
|
|
856
|
-
new_capacity =
|
|
934
|
+
new_capacity =
|
|
935
|
+
(std::max<std::size_t>)(rounded_capacity.value, (std::max<std::size_t>)(doubled_capacity.value, 16u));
|
|
857
936
|
auto allocator = allocator_t{};
|
|
858
937
|
auto new_elements = allocator.allocate(new_capacity);
|
|
859
938
|
if (!new_elements)
|
|
@@ -1018,10 +1097,14 @@ class sorted_buffer_gt {
|
|
|
1018
1097
|
if (new_capacity <= capacity_)
|
|
1019
1098
|
return true;
|
|
1020
1099
|
|
|
1021
|
-
|
|
1022
|
-
if (
|
|
1100
|
+
checked_size_result_t rounded_capacity = checked_ceil2(new_capacity);
|
|
1101
|
+
if (!rounded_capacity)
|
|
1023
1102
|
return false;
|
|
1024
|
-
|
|
1103
|
+
checked_size_result_t doubled_capacity = checked_mul(capacity_, std::size_t{2});
|
|
1104
|
+
if (!doubled_capacity)
|
|
1105
|
+
return false;
|
|
1106
|
+
new_capacity =
|
|
1107
|
+
(std::max<std::size_t>)(rounded_capacity.value, (std::max<std::size_t>)(doubled_capacity.value, 16u));
|
|
1025
1108
|
auto allocator = allocator_t{};
|
|
1026
1109
|
auto new_elements = allocator.allocate(new_capacity);
|
|
1027
1110
|
if (!new_elements)
|
|
@@ -1246,9 +1329,11 @@ class growing_hash_set_gt {
|
|
|
1246
1329
|
count_ = 0;
|
|
1247
1330
|
}
|
|
1248
1331
|
|
|
1249
|
-
growing_hash_set_gt(std::size_t capacity) noexcept
|
|
1250
|
-
|
|
1251
|
-
|
|
1332
|
+
growing_hash_set_gt(std::size_t capacity) noexcept : count_(0u) {
|
|
1333
|
+
checked_size_result_t slots_count = checked_ceil2(capacity);
|
|
1334
|
+
checked_size_result_t bytes = slots_count ? checked_mul(slots_count.value, sizeof(element_t)) : slots_count;
|
|
1335
|
+
slots_ = bytes ? (element_t*)allocator_t{}.allocate(bytes.value) : nullptr;
|
|
1336
|
+
capacity_ = slots_ ? slots_count.value : 0u;
|
|
1252
1337
|
clear();
|
|
1253
1338
|
}
|
|
1254
1339
|
|
|
@@ -1306,12 +1391,21 @@ class growing_hash_set_gt {
|
|
|
1306
1391
|
* @return `true` if enough capacity is available, `false` if memory allocation failed.
|
|
1307
1392
|
*/
|
|
1308
1393
|
bool reserve(std::size_t new_capacity) noexcept {
|
|
1309
|
-
|
|
1394
|
+
checked_size_result_t scaled_capacity = checked_mul(new_capacity, std::size_t{5});
|
|
1395
|
+
if (!scaled_capacity)
|
|
1396
|
+
return false;
|
|
1397
|
+
new_capacity = scaled_capacity.value / 3u;
|
|
1310
1398
|
if (new_capacity <= capacity_)
|
|
1311
1399
|
return true;
|
|
1312
1400
|
|
|
1313
|
-
|
|
1314
|
-
|
|
1401
|
+
checked_size_result_t rounded_capacity = checked_ceil2(new_capacity);
|
|
1402
|
+
if (!rounded_capacity)
|
|
1403
|
+
return false;
|
|
1404
|
+
new_capacity = rounded_capacity.value;
|
|
1405
|
+
checked_size_result_t new_bytes = checked_mul(new_capacity, sizeof(element_t));
|
|
1406
|
+
if (!new_bytes)
|
|
1407
|
+
return false;
|
|
1408
|
+
element_t* new_slots = (element_t*)allocator_t{}.allocate(new_bytes.value);
|
|
1315
1409
|
if (!new_slots)
|
|
1316
1410
|
return false;
|
|
1317
1411
|
|
|
@@ -1414,7 +1508,10 @@ class ring_gt {
|
|
|
1414
1508
|
return false; // prevent data loss
|
|
1415
1509
|
if (n <= capacity())
|
|
1416
1510
|
return true;
|
|
1417
|
-
|
|
1511
|
+
checked_size_result_t rounded_capacity = checked_ceil2(n);
|
|
1512
|
+
if (!rounded_capacity)
|
|
1513
|
+
return false;
|
|
1514
|
+
n = (std::max<std::size_t>)(rounded_capacity.value, 64u);
|
|
1418
1515
|
element_t* elements = allocator_.allocate(n);
|
|
1419
1516
|
if (!elements)
|
|
1420
1517
|
return false;
|
|
@@ -1443,8 +1540,7 @@ class ring_gt {
|
|
|
1443
1540
|
bool try_push(element_t const& value) noexcept {
|
|
1444
1541
|
if (head_ == tail_ && !empty_)
|
|
1445
1542
|
return false; // `elements_` is full
|
|
1446
|
-
|
|
1447
|
-
return push(value);
|
|
1543
|
+
push(value);
|
|
1448
1544
|
return true;
|
|
1449
1545
|
}
|
|
1450
1546
|
|
|
@@ -1504,12 +1600,22 @@ struct index_config_t {
|
|
|
1504
1600
|
inline error_t validate() noexcept {
|
|
1505
1601
|
if (connectivity == 0)
|
|
1506
1602
|
connectivity = default_connectivity();
|
|
1507
|
-
if (connectivity_base == 0)
|
|
1508
|
-
|
|
1603
|
+
if (connectivity_base == 0) {
|
|
1604
|
+
checked_size_result_t default_base = checked_mul(connectivity, std::size_t{2});
|
|
1605
|
+
if (!default_base)
|
|
1606
|
+
return "Connectivity is too large";
|
|
1607
|
+
connectivity_base = default_base.value;
|
|
1608
|
+
}
|
|
1509
1609
|
if (connectivity < 2)
|
|
1510
1610
|
return "Connectivity must be at least 2, otherwise the index degenerates into ropes";
|
|
1511
1611
|
if (connectivity_base < connectivity)
|
|
1512
1612
|
return "Base layer should be at least as connected as the rest of the graph";
|
|
1613
|
+
checked_size_result_t neighbors_bytes =
|
|
1614
|
+
checked_mul_add(connectivity, sizeof(std::uint64_t), sizeof(std::uint32_t));
|
|
1615
|
+
checked_size_result_t neighbors_base_bytes =
|
|
1616
|
+
checked_mul_add(connectivity_base, sizeof(std::uint64_t), sizeof(std::uint32_t));
|
|
1617
|
+
if (!neighbors_bytes || !neighbors_base_bytes)
|
|
1618
|
+
return "Connectivity is too large";
|
|
1513
1619
|
return {};
|
|
1514
1620
|
}
|
|
1515
1621
|
|
|
@@ -1520,6 +1626,15 @@ struct index_config_t {
|
|
|
1520
1626
|
inline bool is_valid() const noexcept { return connectivity >= 2 && connectivity_base >= connectivity; }
|
|
1521
1627
|
};
|
|
1522
1628
|
|
|
1629
|
+
/**
|
|
1630
|
+
* @brief Tag type selecting the "no upfront reservation" overload of
|
|
1631
|
+
* @ref index_limits_t. Modeled after @c std::defer_lock: the
|
|
1632
|
+
* resulting limits are all-zero and produce no allocations when
|
|
1633
|
+
* handed to @ref index_dense_gt::try_reserve.
|
|
1634
|
+
*/
|
|
1635
|
+
struct unreserved_t {};
|
|
1636
|
+
constexpr unreserved_t unreserved{};
|
|
1637
|
+
|
|
1523
1638
|
/**
|
|
1524
1639
|
* @brief Growth settings for the index container.
|
|
1525
1640
|
* Includes the upper bound for `::members` capacity,
|
|
@@ -1527,18 +1642,32 @@ struct index_config_t {
|
|
|
1527
1642
|
*/
|
|
1528
1643
|
struct index_limits_t {
|
|
1529
1644
|
/// @brief Maximum number of entries in the index.
|
|
1530
|
-
std::size_t members
|
|
1645
|
+
std::size_t members;
|
|
1531
1646
|
/// @brief Max number of threads simultaneously updating entries.
|
|
1532
|
-
std::size_t threads_add
|
|
1647
|
+
std::size_t threads_add;
|
|
1533
1648
|
/// @brief Max number of threads simultaneously searching entries.
|
|
1534
|
-
std::size_t threads_search
|
|
1649
|
+
std::size_t threads_search;
|
|
1535
1650
|
|
|
1536
1651
|
inline index_limits_t(std::size_t n, std::size_t t) noexcept : members(n), threads_add(t), threads_search(t) {}
|
|
1537
|
-
inline index_limits_t(std::size_t n = 0) noexcept
|
|
1652
|
+
inline index_limits_t(std::size_t n = 0) noexcept
|
|
1653
|
+
: index_limits_t(n, (std::max<std::size_t>)(1, std::thread::hardware_concurrency())) {}
|
|
1654
|
+
inline index_limits_t(unreserved_t) noexcept : members(0), threads_add(0), threads_search(0) {}
|
|
1538
1655
|
/// @brief Returns the upper limit for the number of threads.
|
|
1539
1656
|
inline std::size_t threads() const noexcept { return (std::max)(threads_add, threads_search); }
|
|
1540
1657
|
/// @brief Returns the concurrency-level of the index - the minimum of thread counts.
|
|
1541
1658
|
inline std::size_t concurrency() const noexcept { return (std::min)(threads_add, threads_search); }
|
|
1659
|
+
/// @brief Returns a copy with zero thread counts replaced by the library default.
|
|
1660
|
+
/// Use when carrying limits forward across operations that may have left
|
|
1661
|
+
/// @c threads_add / @c threads_search unset (e.g. @c unreserved construction).
|
|
1662
|
+
inline index_limits_t with_thread_defaults() const noexcept {
|
|
1663
|
+
index_limits_t result = *this;
|
|
1664
|
+
index_limits_t const defaults;
|
|
1665
|
+
if (!result.threads_add)
|
|
1666
|
+
result.threads_add = defaults.threads_add;
|
|
1667
|
+
if (!result.threads_search)
|
|
1668
|
+
result.threads_search = defaults.threads_search;
|
|
1669
|
+
return result;
|
|
1670
|
+
}
|
|
1542
1671
|
};
|
|
1543
1672
|
|
|
1544
1673
|
struct index_update_config_t {
|
|
@@ -2414,6 +2543,14 @@ class index_gt {
|
|
|
2414
2543
|
/// @brief Array of thread-specific buffers for temporary data.
|
|
2415
2544
|
mutable buffer_gt<context_t, contexts_allocator_t> contexts_{};
|
|
2416
2545
|
|
|
2546
|
+
context_t* context_or_null_(std::size_t thread) noexcept {
|
|
2547
|
+
return thread < contexts_.size() ? contexts_.data() + thread : nullptr;
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
context_t const* context_or_null_(std::size_t thread) const noexcept {
|
|
2551
|
+
return thread < contexts_.size() ? contexts_.data() + thread : nullptr;
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2417
2554
|
public:
|
|
2418
2555
|
std::size_t connectivity() const noexcept { return config_.connectivity; }
|
|
2419
2556
|
std::size_t capacity() const noexcept { return nodes_capacity_; }
|
|
@@ -2547,6 +2684,114 @@ class index_gt {
|
|
|
2547
2684
|
member_iterator_t iterator_at(compressed_slot_t slot) noexcept { return {this, slot}; }
|
|
2548
2685
|
member_citerator_t citerator_at(compressed_slot_t slot) const noexcept { return {this, slot}; }
|
|
2549
2686
|
|
|
2687
|
+
/**
|
|
2688
|
+
* @brief A read-only random-access range over the neighbors of a single
|
|
2689
|
+
* node at a single graph level. Dereferencing yields a `member_cref_t`,
|
|
2690
|
+
* so callers can chain traversals without touching internal slots.
|
|
2691
|
+
*
|
|
2692
|
+
* @warning The range aliases the node's adjacency tape. It is only valid
|
|
2693
|
+
* while the index is not being mutated. Prefer immutable indexes
|
|
2694
|
+
* (see `is_immutable()`) or guarantee no concurrent `add`/`update`/
|
|
2695
|
+
* `remove` while the view is alive.
|
|
2696
|
+
*/
|
|
2697
|
+
class neighbors_view_t {
|
|
2698
|
+
index_gt const* index_{};
|
|
2699
|
+
neighbors_ref_t neighbors_{nullptr};
|
|
2700
|
+
|
|
2701
|
+
public:
|
|
2702
|
+
class const_iterator {
|
|
2703
|
+
index_gt const* index_{};
|
|
2704
|
+
misaligned_ptr_gt<compressed_slot_t const> position_{nullptr};
|
|
2705
|
+
|
|
2706
|
+
public:
|
|
2707
|
+
using iterator_category = std::random_access_iterator_tag;
|
|
2708
|
+
using value_type = member_cref_t;
|
|
2709
|
+
using difference_type = std::ptrdiff_t;
|
|
2710
|
+
using pointer = void;
|
|
2711
|
+
using reference = member_cref_t;
|
|
2712
|
+
|
|
2713
|
+
const_iterator() noexcept = default;
|
|
2714
|
+
const_iterator(index_gt const* index, misaligned_ptr_gt<compressed_slot_t const> position) noexcept
|
|
2715
|
+
: index_(index), position_(position) {}
|
|
2716
|
+
|
|
2717
|
+
reference operator*() const noexcept {
|
|
2718
|
+
compressed_slot_t slot = static_cast<compressed_slot_t>(*position_);
|
|
2719
|
+
return {index_->node_at_(slot).ckey(), slot};
|
|
2720
|
+
}
|
|
2721
|
+
compressed_slot_t slot() const noexcept { return static_cast<compressed_slot_t>(*position_); }
|
|
2722
|
+
|
|
2723
|
+
// clang-format off
|
|
2724
|
+
const_iterator& operator++() noexcept { ++position_; return *this; }
|
|
2725
|
+
const_iterator operator++(int) noexcept { const_iterator old = *this; ++position_; return old; }
|
|
2726
|
+
const_iterator& operator--() noexcept { --position_; return *this; }
|
|
2727
|
+
const_iterator operator--(int) noexcept { const_iterator old = *this; --position_; return old; }
|
|
2728
|
+
const_iterator& operator+=(difference_type d) noexcept { position_ = position_ + d; return *this; }
|
|
2729
|
+
const_iterator& operator-=(difference_type d) noexcept { position_ = position_ - d; return *this; }
|
|
2730
|
+
const_iterator operator+(difference_type d) const noexcept { return {index_, position_ + d}; }
|
|
2731
|
+
const_iterator operator-(difference_type d) const noexcept { return {index_, position_ - d}; }
|
|
2732
|
+
difference_type operator-(const_iterator const& other) const noexcept { return position_ - other.position_; }
|
|
2733
|
+
bool operator==(const_iterator const& other) const noexcept { return position_ == other.position_; }
|
|
2734
|
+
bool operator!=(const_iterator const& other) const noexcept { return position_ != other.position_; }
|
|
2735
|
+
// clang-format on
|
|
2736
|
+
};
|
|
2737
|
+
|
|
2738
|
+
using iterator = const_iterator;
|
|
2739
|
+
using value_type = member_cref_t;
|
|
2740
|
+
using size_type = std::size_t;
|
|
2741
|
+
|
|
2742
|
+
neighbors_view_t() noexcept = default;
|
|
2743
|
+
neighbors_view_t(index_gt const* index, neighbors_ref_t neighbors) noexcept
|
|
2744
|
+
: index_(index), neighbors_(neighbors) {}
|
|
2745
|
+
|
|
2746
|
+
std::size_t size() const noexcept { return index_ ? neighbors_.size() : 0; }
|
|
2747
|
+
bool empty() const noexcept { return size() == 0; }
|
|
2748
|
+
member_cref_t operator[](std::size_t offset) const noexcept {
|
|
2749
|
+
compressed_slot_t slot = neighbors_[offset];
|
|
2750
|
+
return {index_->node_at_(slot).ckey(), slot};
|
|
2751
|
+
}
|
|
2752
|
+
const_iterator begin() const noexcept {
|
|
2753
|
+
return index_ ? const_iterator{index_, neighbors_.begin()} : const_iterator{};
|
|
2754
|
+
}
|
|
2755
|
+
const_iterator end() const noexcept {
|
|
2756
|
+
return index_ ? const_iterator{index_, neighbors_.end()} : const_iterator{};
|
|
2757
|
+
}
|
|
2758
|
+
const_iterator cbegin() const noexcept { return begin(); }
|
|
2759
|
+
const_iterator cend() const noexcept { return end(); }
|
|
2760
|
+
};
|
|
2761
|
+
|
|
2762
|
+
/**
|
|
2763
|
+
* @brief Returns a read-only range over the neighbors of the node at @p slot
|
|
2764
|
+
* in the graph @p level. Returned view is empty when @p level exceeds
|
|
2765
|
+
* the node's level.
|
|
2766
|
+
*/
|
|
2767
|
+
neighbors_view_t neighbors(compressed_slot_t slot, std::size_t level) const noexcept {
|
|
2768
|
+
node_t node = node_at_(slot);
|
|
2769
|
+
if (static_cast<level_t>(level) > node.level())
|
|
2770
|
+
return {};
|
|
2771
|
+
return {this, neighbors_(node, static_cast<level_t>(level))};
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2774
|
+
/**
|
|
2775
|
+
* @brief Returns a read-only range over the neighbors of the node referenced
|
|
2776
|
+
* by @p member at the graph @p level.
|
|
2777
|
+
*/
|
|
2778
|
+
neighbors_view_t neighbors(member_citerator_t member, std::size_t level) const noexcept {
|
|
2779
|
+
return neighbors(get_slot(member), level);
|
|
2780
|
+
}
|
|
2781
|
+
|
|
2782
|
+
/**
|
|
2783
|
+
* @brief Returns the top graph level at which the node at @p slot is present.
|
|
2784
|
+
*/
|
|
2785
|
+
std::size_t level_of(compressed_slot_t slot) const noexcept {
|
|
2786
|
+
return static_cast<std::size_t>(static_cast<level_t>(node_at_(slot).level()));
|
|
2787
|
+
}
|
|
2788
|
+
|
|
2789
|
+
/**
|
|
2790
|
+
* @brief Returns the top graph level at which the node referenced by @p member
|
|
2791
|
+
* is present.
|
|
2792
|
+
*/
|
|
2793
|
+
std::size_t level_of(member_citerator_t member) const noexcept { return level_of(get_slot(member)); }
|
|
2794
|
+
|
|
2550
2795
|
dynamic_allocator_t const& dynamic_allocator() const noexcept { return dynamic_allocator_; }
|
|
2551
2796
|
tape_allocator_t const& tape_allocator() const noexcept { return tape_allocator_; }
|
|
2552
2797
|
|
|
@@ -2919,7 +3164,10 @@ class index_gt {
|
|
|
2919
3164
|
return result.failed("Can't add to an immutable index");
|
|
2920
3165
|
|
|
2921
3166
|
// Make sure we have enough local memory to perform this request
|
|
2922
|
-
context_t
|
|
3167
|
+
context_t* context_ptr = context_or_null_(config.thread);
|
|
3168
|
+
if (!context_ptr)
|
|
3169
|
+
return result.failed("Reserve capacity ahead of insertions!");
|
|
3170
|
+
context_t& context = *context_ptr;
|
|
2923
3171
|
top_candidates_t& top = context.top_candidates;
|
|
2924
3172
|
next_candidates_t& next = context.next_candidates;
|
|
2925
3173
|
top.clear();
|
|
@@ -3057,7 +3305,10 @@ class index_gt {
|
|
|
3057
3305
|
compressed_slot_t updated_slot = iterator.slot_;
|
|
3058
3306
|
|
|
3059
3307
|
// Make sure we have enough local memory to perform this request
|
|
3060
|
-
context_t
|
|
3308
|
+
context_t* context_ptr = context_or_null_(config.thread);
|
|
3309
|
+
if (!context_ptr)
|
|
3310
|
+
return result.failed("Reserve capacity ahead of updates!");
|
|
3311
|
+
context_t& context = *context_ptr;
|
|
3061
3312
|
top_candidates_t& top = context.top_candidates;
|
|
3062
3313
|
next_candidates_t& next = context.next_candidates;
|
|
3063
3314
|
top.clear();
|
|
@@ -3425,26 +3676,31 @@ class index_gt {
|
|
|
3425
3676
|
|
|
3426
3677
|
// Progress status
|
|
3427
3678
|
std::size_t processed = 0;
|
|
3428
|
-
|
|
3679
|
+
checked_size_result_t header_size = checked_size_from_u64(header.size);
|
|
3680
|
+
if (!header_size)
|
|
3681
|
+
return result.failed("Index is too large to serialize");
|
|
3682
|
+
checked_size_result_t total = checked_mul(std::size_t{2}, header_size.value);
|
|
3683
|
+
if (!total)
|
|
3684
|
+
return result.failed("Index is too large to serialize");
|
|
3429
3685
|
|
|
3430
3686
|
// Export the number of levels per node
|
|
3431
3687
|
// That is both enough to estimate the overall memory consumption,
|
|
3432
3688
|
// and to be able to estimate the offsets of every entry in the file.
|
|
3433
|
-
for (std::size_t i = 0; i !=
|
|
3689
|
+
for (std::size_t i = 0; i != header_size.value; ++i) {
|
|
3434
3690
|
node_t node = node_at_(i);
|
|
3435
3691
|
level_t level = node.level();
|
|
3436
3692
|
if (!output(&level, sizeof(level)))
|
|
3437
3693
|
return result.failed("Failed to serialize into stream");
|
|
3438
|
-
if (!progress(++processed, total))
|
|
3694
|
+
if (!progress(++processed, total.value))
|
|
3439
3695
|
return result.failed("Terminated by user");
|
|
3440
3696
|
}
|
|
3441
3697
|
|
|
3442
3698
|
// After that dump the nodes themselves
|
|
3443
|
-
for (std::size_t i = 0; i !=
|
|
3699
|
+
for (std::size_t i = 0; i != header_size.value; ++i) {
|
|
3444
3700
|
span_bytes_t node_bytes = node_bytes_(node_at_(i));
|
|
3445
3701
|
if (!output(node_bytes.data(), node_bytes.size()))
|
|
3446
3702
|
return result.failed("Failed to serialize into stream");
|
|
3447
|
-
if (!progress(++processed, total))
|
|
3703
|
+
if (!progress(++processed, total.value))
|
|
3448
3704
|
return result.failed("Terminated by user");
|
|
3449
3705
|
}
|
|
3450
3706
|
|
|
@@ -3476,10 +3732,16 @@ class index_gt {
|
|
|
3476
3732
|
|
|
3477
3733
|
// Allocate some dynamic memory to read all the levels
|
|
3478
3734
|
using levels_allocator_t = typename dynamic_allocator_traits_t::template rebind_alloc<level_t>;
|
|
3479
|
-
|
|
3735
|
+
checked_size_result_t header_size = checked_size_from_u64(header.size);
|
|
3736
|
+
if (!header_size)
|
|
3737
|
+
return result.failed("Index is too large");
|
|
3738
|
+
buffer_gt<level_t, levels_allocator_t> levels(header_size.value);
|
|
3480
3739
|
if (!levels)
|
|
3481
3740
|
return result.failed("Out of memory");
|
|
3482
|
-
|
|
3741
|
+
checked_size_result_t levels_bytes = checked_mul(header_size.value, sizeof(level_t));
|
|
3742
|
+
if (!levels_bytes)
|
|
3743
|
+
return result.failed("Index is too large");
|
|
3744
|
+
if (!input(levels, levels_bytes.value))
|
|
3483
3745
|
return result.failed("Failed to pull nodes levels from the stream");
|
|
3484
3746
|
|
|
3485
3747
|
// Submit metadata
|
|
@@ -3491,26 +3753,26 @@ class index_gt {
|
|
|
3491
3753
|
|
|
3492
3754
|
pre_ = precompute_(config_);
|
|
3493
3755
|
index_limits_t limits;
|
|
3494
|
-
limits.members =
|
|
3756
|
+
limits.members = header_size.value;
|
|
3495
3757
|
limits.threads_add = (std::max<std::size_t>)(1, old_limits.threads_add);
|
|
3496
3758
|
limits.threads_search = (std::max<std::size_t>)(1, old_limits.threads_search);
|
|
3497
3759
|
if (!reserve(limits)) {
|
|
3498
3760
|
reset();
|
|
3499
3761
|
return result.failed("Out of memory");
|
|
3500
3762
|
}
|
|
3501
|
-
nodes_count_ =
|
|
3763
|
+
nodes_count_ = header_size.value;
|
|
3502
3764
|
max_level_ = static_cast<level_t>(header.max_level);
|
|
3503
3765
|
entry_slot_ = static_cast<compressed_slot_t>(header.entry_slot);
|
|
3504
3766
|
|
|
3505
3767
|
// Load the nodes
|
|
3506
|
-
for (std::size_t i = 0; i !=
|
|
3768
|
+
for (std::size_t i = 0; i != header_size.value; ++i) {
|
|
3507
3769
|
span_bytes_t node_bytes = node_malloc_(levels[i]);
|
|
3508
3770
|
if (!input(node_bytes.data(), node_bytes.size())) {
|
|
3509
3771
|
reset();
|
|
3510
3772
|
return result.failed("Failed to pull nodes from the stream");
|
|
3511
3773
|
}
|
|
3512
3774
|
nodes_[i] = node_t{node_bytes.data()};
|
|
3513
|
-
if (!progress(i + 1,
|
|
3775
|
+
if (!progress(i + 1, header_size.value))
|
|
3514
3776
|
return result.failed("Terminated by user");
|
|
3515
3777
|
}
|
|
3516
3778
|
return {};
|
|
@@ -3657,11 +3919,14 @@ class index_gt {
|
|
|
3657
3919
|
reset();
|
|
3658
3920
|
return result;
|
|
3659
3921
|
}
|
|
3922
|
+
checked_size_result_t header_size = checked_size_from_u64(header.size);
|
|
3923
|
+
if (!header_size)
|
|
3924
|
+
return result.failed("Index is too large");
|
|
3660
3925
|
|
|
3661
3926
|
// Precompute offsets of every node, but before that we need to update the configs
|
|
3662
3927
|
// This could have been done with `std::exclusive_scan`, but it's only available from C++17.
|
|
3663
3928
|
using offsets_allocator_t = typename dynamic_allocator_traits_t::template rebind_alloc<std::size_t>;
|
|
3664
|
-
buffer_gt<std::size_t, offsets_allocator_t> offsets(
|
|
3929
|
+
buffer_gt<std::size_t, offsets_allocator_t> offsets(header_size.value);
|
|
3665
3930
|
if (!offsets)
|
|
3666
3931
|
return result.failed("Out of memory");
|
|
3667
3932
|
|
|
@@ -3673,33 +3938,47 @@ class index_gt {
|
|
|
3673
3938
|
|
|
3674
3939
|
pre_ = precompute_(config_);
|
|
3675
3940
|
misaligned_ptr_gt<level_t> levels{(byte_t*)file.data() + offset + sizeof(header)};
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3941
|
+
checked_size_result_t levels_bytes = checked_mul(sizeof(level_t), header_size.value);
|
|
3942
|
+
checked_size_result_t offset_after_header = checked_add(offset, sizeof(header));
|
|
3943
|
+
checked_size_result_t first_offset = levels_bytes && offset_after_header
|
|
3944
|
+
? checked_add(offset_after_header.value, levels_bytes.value)
|
|
3945
|
+
: checked_size_overflow();
|
|
3946
|
+
if (!first_offset)
|
|
3947
|
+
return result.failed("Index is too large");
|
|
3948
|
+
offsets[0u] = first_offset.value;
|
|
3949
|
+
for (std::size_t i = 1; i < header_size.value; ++i) {
|
|
3950
|
+
checked_size_result_t next_offset = checked_add(offsets[i - 1], node_bytes_(levels[i - 1]));
|
|
3951
|
+
if (!next_offset)
|
|
3952
|
+
return result.failed("Index is too large");
|
|
3953
|
+
offsets[i] = next_offset.value;
|
|
3954
|
+
}
|
|
3679
3955
|
|
|
3680
|
-
|
|
3681
|
-
|
|
3956
|
+
checked_size_result_t total_bytes =
|
|
3957
|
+
checked_add(offsets[header_size.value - 1], node_bytes_(levels[header_size.value - 1]));
|
|
3958
|
+
if (!total_bytes)
|
|
3959
|
+
return result.failed("Index is too large");
|
|
3960
|
+
if (file.size() < total_bytes.value) {
|
|
3682
3961
|
reset();
|
|
3683
3962
|
return result.failed("File is corrupted and can't fit all the nodes");
|
|
3684
3963
|
}
|
|
3685
3964
|
|
|
3686
3965
|
// Submit metadata and reserve memory
|
|
3687
3966
|
index_limits_t limits;
|
|
3688
|
-
limits.members =
|
|
3967
|
+
limits.members = header_size.value;
|
|
3689
3968
|
limits.threads_add = (std::max<std::size_t>)(1, old_limits.threads_add);
|
|
3690
3969
|
limits.threads_search = (std::max<std::size_t>)(1, old_limits.threads_search);
|
|
3691
3970
|
if (!reserve(limits)) {
|
|
3692
3971
|
reset();
|
|
3693
3972
|
return result.failed("Out of memory");
|
|
3694
3973
|
}
|
|
3695
|
-
nodes_count_ =
|
|
3974
|
+
nodes_count_ = header_size.value;
|
|
3696
3975
|
max_level_ = static_cast<level_t>(header.max_level);
|
|
3697
3976
|
entry_slot_ = static_cast<compressed_slot_t>(header.entry_slot);
|
|
3698
3977
|
|
|
3699
3978
|
// Rapidly address all the nodes
|
|
3700
|
-
for (std::size_t i = 0; i !=
|
|
3979
|
+
for (std::size_t i = 0; i != header_size.value; ++i) {
|
|
3701
3980
|
nodes_[i] = node_t{(byte_t*)file.data() + offsets[i]};
|
|
3702
|
-
if (!progress(i + 1,
|
|
3981
|
+
if (!progress(i + 1, header_size.value))
|
|
3703
3982
|
return result.failed("Terminated by user");
|
|
3704
3983
|
}
|
|
3705
3984
|
viewed_file_ = std::move(file);
|
|
@@ -3751,7 +4030,9 @@ class index_gt {
|
|
|
3751
4030
|
// Progress status
|
|
3752
4031
|
std::atomic<bool> do_tasks{true};
|
|
3753
4032
|
std::atomic<std::size_t> processed{0};
|
|
3754
|
-
|
|
4033
|
+
checked_size_result_t total = checked_mul(std::size_t{3}, slots_and_levels.size());
|
|
4034
|
+
if (!total)
|
|
4035
|
+
return;
|
|
3755
4036
|
|
|
3756
4037
|
// For every bottom level node, determine its parent cluster
|
|
3757
4038
|
executor.dynamic(slots_and_levels.size(), [&](std::size_t thread_idx, std::size_t old_slot_as_uint) {
|
|
@@ -3764,7 +4045,7 @@ class index_gt {
|
|
|
3764
4045
|
slots_and_levels[old_slot] = {old_slot, cluster, node_at_(old_slot).level()};
|
|
3765
4046
|
++processed;
|
|
3766
4047
|
if (thread_idx == 0)
|
|
3767
|
-
do_tasks = progress(processed.load(), total);
|
|
4048
|
+
do_tasks = progress(processed.load(), total.value);
|
|
3768
4049
|
return do_tasks.load();
|
|
3769
4050
|
});
|
|
3770
4051
|
if (!do_tasks.load())
|
|
@@ -3798,7 +4079,7 @@ class index_gt {
|
|
|
3798
4079
|
neighbor = static_cast<compressed_slot_t>(old_slot_to_new[compressed_slot_t(neighbor)]);
|
|
3799
4080
|
|
|
3800
4081
|
reordered_nodes[new_slot] = new_node;
|
|
3801
|
-
if (!progress(++processed, total))
|
|
4082
|
+
if (!progress(++processed, total.value))
|
|
3802
4083
|
return;
|
|
3803
4084
|
}
|
|
3804
4085
|
|
|
@@ -3807,7 +4088,7 @@ class index_gt {
|
|
|
3807
4088
|
slot_transition(node_at_(old_slot).ckey(), //
|
|
3808
4089
|
static_cast<compressed_slot_t>(old_slot), //
|
|
3809
4090
|
static_cast<compressed_slot_t>(new_slot));
|
|
3810
|
-
if (!progress(++processed, total))
|
|
4091
|
+
if (!progress(++processed, total.value))
|
|
3811
4092
|
return;
|
|
3812
4093
|
}
|
|
3813
4094
|
|