usearch 2.25.1 → 2.26.0
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 +2 -2
- package/c/lib.cpp +28 -13
- package/cpp/test.cpp +169 -0
- package/include/usearch/index.hpp +364 -58
- package/include/usearch/index_dense.hpp +155 -50
- package/include/usearch/index_plugins.hpp +156 -79
- package/javascript/lib.cpp +4 -2
- 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 +2 -2
- 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
- package/prebuilds/win32-x64/usearch.node +0 -0
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
#define UNUM_USEARCH_HPP
|
|
9
9
|
|
|
10
10
|
#define USEARCH_VERSION_MAJOR 2
|
|
11
|
-
#define USEARCH_VERSION_MINOR
|
|
12
|
-
#define USEARCH_VERSION_PATCH
|
|
11
|
+
#define USEARCH_VERSION_MINOR 26
|
|
12
|
+
#define USEARCH_VERSION_PATCH 0
|
|
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)
|
|
@@ -1194,6 +1277,31 @@ template <typename element_at> struct hash_gt {
|
|
|
1194
1277
|
std::size_t operator()(element_at const& element) const noexcept { return std::hash<element_at>{}(element); }
|
|
1195
1278
|
};
|
|
1196
1279
|
|
|
1280
|
+
/**
|
|
1281
|
+
* @brief SplitMix64 finalizer, used to scatter integer keys before masking.
|
|
1282
|
+
*
|
|
1283
|
+
* On libstdc++ and libc++ `std::hash` is the identity for integers. Our open-addressing
|
|
1284
|
+
* tables mask that hash into a power-of-2 slot count and probe linearly until an @b empty
|
|
1285
|
+
* slot, so consecutive keys land in adjacent slots and merge into one contiguous run.
|
|
1286
|
+
* Both insertions and lookups then scan that run end-to-end, which is quadratic overall:
|
|
1287
|
+
* a dense ascending key range collapses insertion throughput by three orders of magnitude.
|
|
1288
|
+
* Mixing costs ~20ns per key and is dwarfed by the graph traversal in `add`.
|
|
1289
|
+
*/
|
|
1290
|
+
template <> struct hash_gt<std::uint64_t> {
|
|
1291
|
+
std::size_t operator()(std::uint64_t const& element) const noexcept {
|
|
1292
|
+
std::uint64_t x = element;
|
|
1293
|
+
x = (x ^ (x >> 30u)) * 0xBF58476D1CE4E5B9ULL;
|
|
1294
|
+
x = (x ^ (x >> 27u)) * 0x94D049BB133111EBULL;
|
|
1295
|
+
return static_cast<std::size_t>(x ^ (x >> 31u));
|
|
1296
|
+
}
|
|
1297
|
+
};
|
|
1298
|
+
|
|
1299
|
+
template <> struct hash_gt<std::int64_t> {
|
|
1300
|
+
std::size_t operator()(std::int64_t const& element) const noexcept {
|
|
1301
|
+
return hash_gt<std::uint64_t>{}(static_cast<std::uint64_t>(element));
|
|
1302
|
+
}
|
|
1303
|
+
};
|
|
1304
|
+
|
|
1197
1305
|
template <> struct hash_gt<uint40_t> {
|
|
1198
1306
|
std::size_t operator()(uint40_t const& element) const noexcept { return std::hash<std::size_t>{}(element); }
|
|
1199
1307
|
};
|
|
@@ -1246,9 +1354,11 @@ class growing_hash_set_gt {
|
|
|
1246
1354
|
count_ = 0;
|
|
1247
1355
|
}
|
|
1248
1356
|
|
|
1249
|
-
growing_hash_set_gt(std::size_t capacity) noexcept
|
|
1250
|
-
|
|
1251
|
-
|
|
1357
|
+
growing_hash_set_gt(std::size_t capacity) noexcept : count_(0u) {
|
|
1358
|
+
checked_size_result_t slots_count = checked_ceil2(capacity);
|
|
1359
|
+
checked_size_result_t bytes = slots_count ? checked_mul(slots_count.value, sizeof(element_t)) : slots_count;
|
|
1360
|
+
slots_ = bytes ? (element_t*)allocator_t{}.allocate(bytes.value) : nullptr;
|
|
1361
|
+
capacity_ = slots_ ? slots_count.value : 0u;
|
|
1252
1362
|
clear();
|
|
1253
1363
|
}
|
|
1254
1364
|
|
|
@@ -1306,12 +1416,21 @@ class growing_hash_set_gt {
|
|
|
1306
1416
|
* @return `true` if enough capacity is available, `false` if memory allocation failed.
|
|
1307
1417
|
*/
|
|
1308
1418
|
bool reserve(std::size_t new_capacity) noexcept {
|
|
1309
|
-
|
|
1419
|
+
checked_size_result_t scaled_capacity = checked_mul(new_capacity, std::size_t{5});
|
|
1420
|
+
if (!scaled_capacity)
|
|
1421
|
+
return false;
|
|
1422
|
+
new_capacity = scaled_capacity.value / 3u;
|
|
1310
1423
|
if (new_capacity <= capacity_)
|
|
1311
1424
|
return true;
|
|
1312
1425
|
|
|
1313
|
-
|
|
1314
|
-
|
|
1426
|
+
checked_size_result_t rounded_capacity = checked_ceil2(new_capacity);
|
|
1427
|
+
if (!rounded_capacity)
|
|
1428
|
+
return false;
|
|
1429
|
+
new_capacity = rounded_capacity.value;
|
|
1430
|
+
checked_size_result_t new_bytes = checked_mul(new_capacity, sizeof(element_t));
|
|
1431
|
+
if (!new_bytes)
|
|
1432
|
+
return false;
|
|
1433
|
+
element_t* new_slots = (element_t*)allocator_t{}.allocate(new_bytes.value);
|
|
1315
1434
|
if (!new_slots)
|
|
1316
1435
|
return false;
|
|
1317
1436
|
|
|
@@ -1414,7 +1533,10 @@ class ring_gt {
|
|
|
1414
1533
|
return false; // prevent data loss
|
|
1415
1534
|
if (n <= capacity())
|
|
1416
1535
|
return true;
|
|
1417
|
-
|
|
1536
|
+
checked_size_result_t rounded_capacity = checked_ceil2(n);
|
|
1537
|
+
if (!rounded_capacity)
|
|
1538
|
+
return false;
|
|
1539
|
+
n = (std::max<std::size_t>)(rounded_capacity.value, 64u);
|
|
1418
1540
|
element_t* elements = allocator_.allocate(n);
|
|
1419
1541
|
if (!elements)
|
|
1420
1542
|
return false;
|
|
@@ -1443,8 +1565,7 @@ class ring_gt {
|
|
|
1443
1565
|
bool try_push(element_t const& value) noexcept {
|
|
1444
1566
|
if (head_ == tail_ && !empty_)
|
|
1445
1567
|
return false; // `elements_` is full
|
|
1446
|
-
|
|
1447
|
-
return push(value);
|
|
1568
|
+
push(value);
|
|
1448
1569
|
return true;
|
|
1449
1570
|
}
|
|
1450
1571
|
|
|
@@ -1504,12 +1625,22 @@ struct index_config_t {
|
|
|
1504
1625
|
inline error_t validate() noexcept {
|
|
1505
1626
|
if (connectivity == 0)
|
|
1506
1627
|
connectivity = default_connectivity();
|
|
1507
|
-
if (connectivity_base == 0)
|
|
1508
|
-
|
|
1628
|
+
if (connectivity_base == 0) {
|
|
1629
|
+
checked_size_result_t default_base = checked_mul(connectivity, std::size_t{2});
|
|
1630
|
+
if (!default_base)
|
|
1631
|
+
return "Connectivity is too large";
|
|
1632
|
+
connectivity_base = default_base.value;
|
|
1633
|
+
}
|
|
1509
1634
|
if (connectivity < 2)
|
|
1510
1635
|
return "Connectivity must be at least 2, otherwise the index degenerates into ropes";
|
|
1511
1636
|
if (connectivity_base < connectivity)
|
|
1512
1637
|
return "Base layer should be at least as connected as the rest of the graph";
|
|
1638
|
+
checked_size_result_t neighbors_bytes =
|
|
1639
|
+
checked_mul_add(connectivity, sizeof(std::uint64_t), sizeof(std::uint32_t));
|
|
1640
|
+
checked_size_result_t neighbors_base_bytes =
|
|
1641
|
+
checked_mul_add(connectivity_base, sizeof(std::uint64_t), sizeof(std::uint32_t));
|
|
1642
|
+
if (!neighbors_bytes || !neighbors_base_bytes)
|
|
1643
|
+
return "Connectivity is too large";
|
|
1513
1644
|
return {};
|
|
1514
1645
|
}
|
|
1515
1646
|
|
|
@@ -1520,6 +1651,15 @@ struct index_config_t {
|
|
|
1520
1651
|
inline bool is_valid() const noexcept { return connectivity >= 2 && connectivity_base >= connectivity; }
|
|
1521
1652
|
};
|
|
1522
1653
|
|
|
1654
|
+
/**
|
|
1655
|
+
* @brief Tag type selecting the "no upfront reservation" overload of
|
|
1656
|
+
* @ref index_limits_t. Modeled after @c std::defer_lock: the
|
|
1657
|
+
* resulting limits are all-zero and produce no allocations when
|
|
1658
|
+
* handed to @ref index_dense_gt::try_reserve.
|
|
1659
|
+
*/
|
|
1660
|
+
struct unreserved_t {};
|
|
1661
|
+
constexpr unreserved_t unreserved{};
|
|
1662
|
+
|
|
1523
1663
|
/**
|
|
1524
1664
|
* @brief Growth settings for the index container.
|
|
1525
1665
|
* Includes the upper bound for `::members` capacity,
|
|
@@ -1527,18 +1667,32 @@ struct index_config_t {
|
|
|
1527
1667
|
*/
|
|
1528
1668
|
struct index_limits_t {
|
|
1529
1669
|
/// @brief Maximum number of entries in the index.
|
|
1530
|
-
std::size_t members
|
|
1670
|
+
std::size_t members;
|
|
1531
1671
|
/// @brief Max number of threads simultaneously updating entries.
|
|
1532
|
-
std::size_t threads_add
|
|
1672
|
+
std::size_t threads_add;
|
|
1533
1673
|
/// @brief Max number of threads simultaneously searching entries.
|
|
1534
|
-
std::size_t threads_search
|
|
1674
|
+
std::size_t threads_search;
|
|
1535
1675
|
|
|
1536
1676
|
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
|
|
1677
|
+
inline index_limits_t(std::size_t n = 0) noexcept
|
|
1678
|
+
: index_limits_t(n, (std::max<std::size_t>)(1, std::thread::hardware_concurrency())) {}
|
|
1679
|
+
inline index_limits_t(unreserved_t) noexcept : members(0), threads_add(0), threads_search(0) {}
|
|
1538
1680
|
/// @brief Returns the upper limit for the number of threads.
|
|
1539
1681
|
inline std::size_t threads() const noexcept { return (std::max)(threads_add, threads_search); }
|
|
1540
1682
|
/// @brief Returns the concurrency-level of the index - the minimum of thread counts.
|
|
1541
1683
|
inline std::size_t concurrency() const noexcept { return (std::min)(threads_add, threads_search); }
|
|
1684
|
+
/// @brief Returns a copy with zero thread counts replaced by the library default.
|
|
1685
|
+
/// Use when carrying limits forward across operations that may have left
|
|
1686
|
+
/// @c threads_add / @c threads_search unset (e.g. @c unreserved construction).
|
|
1687
|
+
inline index_limits_t with_thread_defaults() const noexcept {
|
|
1688
|
+
index_limits_t result = *this;
|
|
1689
|
+
index_limits_t const defaults;
|
|
1690
|
+
if (!result.threads_add)
|
|
1691
|
+
result.threads_add = defaults.threads_add;
|
|
1692
|
+
if (!result.threads_search)
|
|
1693
|
+
result.threads_search = defaults.threads_search;
|
|
1694
|
+
return result;
|
|
1695
|
+
}
|
|
1542
1696
|
};
|
|
1543
1697
|
|
|
1544
1698
|
struct index_update_config_t {
|
|
@@ -2414,6 +2568,14 @@ class index_gt {
|
|
|
2414
2568
|
/// @brief Array of thread-specific buffers for temporary data.
|
|
2415
2569
|
mutable buffer_gt<context_t, contexts_allocator_t> contexts_{};
|
|
2416
2570
|
|
|
2571
|
+
context_t* context_or_null_(std::size_t thread) noexcept {
|
|
2572
|
+
return thread < contexts_.size() ? contexts_.data() + thread : nullptr;
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2575
|
+
context_t const* context_or_null_(std::size_t thread) const noexcept {
|
|
2576
|
+
return thread < contexts_.size() ? contexts_.data() + thread : nullptr;
|
|
2577
|
+
}
|
|
2578
|
+
|
|
2417
2579
|
public:
|
|
2418
2580
|
std::size_t connectivity() const noexcept { return config_.connectivity; }
|
|
2419
2581
|
std::size_t capacity() const noexcept { return nodes_capacity_; }
|
|
@@ -2547,6 +2709,114 @@ class index_gt {
|
|
|
2547
2709
|
member_iterator_t iterator_at(compressed_slot_t slot) noexcept { return {this, slot}; }
|
|
2548
2710
|
member_citerator_t citerator_at(compressed_slot_t slot) const noexcept { return {this, slot}; }
|
|
2549
2711
|
|
|
2712
|
+
/**
|
|
2713
|
+
* @brief A read-only random-access range over the neighbors of a single
|
|
2714
|
+
* node at a single graph level. Dereferencing yields a `member_cref_t`,
|
|
2715
|
+
* so callers can chain traversals without touching internal slots.
|
|
2716
|
+
*
|
|
2717
|
+
* @warning The range aliases the node's adjacency tape. It is only valid
|
|
2718
|
+
* while the index is not being mutated. Prefer immutable indexes
|
|
2719
|
+
* (see `is_immutable()`) or guarantee no concurrent `add`/`update`/
|
|
2720
|
+
* `remove` while the view is alive.
|
|
2721
|
+
*/
|
|
2722
|
+
class neighbors_view_t {
|
|
2723
|
+
index_gt const* index_{};
|
|
2724
|
+
neighbors_ref_t neighbors_{nullptr};
|
|
2725
|
+
|
|
2726
|
+
public:
|
|
2727
|
+
class const_iterator {
|
|
2728
|
+
index_gt const* index_{};
|
|
2729
|
+
misaligned_ptr_gt<compressed_slot_t const> position_{nullptr};
|
|
2730
|
+
|
|
2731
|
+
public:
|
|
2732
|
+
using iterator_category = std::random_access_iterator_tag;
|
|
2733
|
+
using value_type = member_cref_t;
|
|
2734
|
+
using difference_type = std::ptrdiff_t;
|
|
2735
|
+
using pointer = void;
|
|
2736
|
+
using reference = member_cref_t;
|
|
2737
|
+
|
|
2738
|
+
const_iterator() noexcept = default;
|
|
2739
|
+
const_iterator(index_gt const* index, misaligned_ptr_gt<compressed_slot_t const> position) noexcept
|
|
2740
|
+
: index_(index), position_(position) {}
|
|
2741
|
+
|
|
2742
|
+
reference operator*() const noexcept {
|
|
2743
|
+
compressed_slot_t slot = static_cast<compressed_slot_t>(*position_);
|
|
2744
|
+
return {index_->node_at_(slot).ckey(), slot};
|
|
2745
|
+
}
|
|
2746
|
+
compressed_slot_t slot() const noexcept { return static_cast<compressed_slot_t>(*position_); }
|
|
2747
|
+
|
|
2748
|
+
// clang-format off
|
|
2749
|
+
const_iterator& operator++() noexcept { ++position_; return *this; }
|
|
2750
|
+
const_iterator operator++(int) noexcept { const_iterator old = *this; ++position_; return old; }
|
|
2751
|
+
const_iterator& operator--() noexcept { --position_; return *this; }
|
|
2752
|
+
const_iterator operator--(int) noexcept { const_iterator old = *this; --position_; return old; }
|
|
2753
|
+
const_iterator& operator+=(difference_type d) noexcept { position_ = position_ + d; return *this; }
|
|
2754
|
+
const_iterator& operator-=(difference_type d) noexcept { position_ = position_ - d; return *this; }
|
|
2755
|
+
const_iterator operator+(difference_type d) const noexcept { return {index_, position_ + d}; }
|
|
2756
|
+
const_iterator operator-(difference_type d) const noexcept { return {index_, position_ - d}; }
|
|
2757
|
+
difference_type operator-(const_iterator const& other) const noexcept { return position_ - other.position_; }
|
|
2758
|
+
bool operator==(const_iterator const& other) const noexcept { return position_ == other.position_; }
|
|
2759
|
+
bool operator!=(const_iterator const& other) const noexcept { return position_ != other.position_; }
|
|
2760
|
+
// clang-format on
|
|
2761
|
+
};
|
|
2762
|
+
|
|
2763
|
+
using iterator = const_iterator;
|
|
2764
|
+
using value_type = member_cref_t;
|
|
2765
|
+
using size_type = std::size_t;
|
|
2766
|
+
|
|
2767
|
+
neighbors_view_t() noexcept = default;
|
|
2768
|
+
neighbors_view_t(index_gt const* index, neighbors_ref_t neighbors) noexcept
|
|
2769
|
+
: index_(index), neighbors_(neighbors) {}
|
|
2770
|
+
|
|
2771
|
+
std::size_t size() const noexcept { return index_ ? neighbors_.size() : 0; }
|
|
2772
|
+
bool empty() const noexcept { return size() == 0; }
|
|
2773
|
+
member_cref_t operator[](std::size_t offset) const noexcept {
|
|
2774
|
+
compressed_slot_t slot = neighbors_[offset];
|
|
2775
|
+
return {index_->node_at_(slot).ckey(), slot};
|
|
2776
|
+
}
|
|
2777
|
+
const_iterator begin() const noexcept {
|
|
2778
|
+
return index_ ? const_iterator{index_, neighbors_.begin()} : const_iterator{};
|
|
2779
|
+
}
|
|
2780
|
+
const_iterator end() const noexcept {
|
|
2781
|
+
return index_ ? const_iterator{index_, neighbors_.end()} : const_iterator{};
|
|
2782
|
+
}
|
|
2783
|
+
const_iterator cbegin() const noexcept { return begin(); }
|
|
2784
|
+
const_iterator cend() const noexcept { return end(); }
|
|
2785
|
+
};
|
|
2786
|
+
|
|
2787
|
+
/**
|
|
2788
|
+
* @brief Returns a read-only range over the neighbors of the node at @p slot
|
|
2789
|
+
* in the graph @p level. Returned view is empty when @p level exceeds
|
|
2790
|
+
* the node's level.
|
|
2791
|
+
*/
|
|
2792
|
+
neighbors_view_t neighbors(compressed_slot_t slot, std::size_t level) const noexcept {
|
|
2793
|
+
node_t node = node_at_(slot);
|
|
2794
|
+
if (static_cast<level_t>(level) > node.level())
|
|
2795
|
+
return {};
|
|
2796
|
+
return {this, neighbors_(node, static_cast<level_t>(level))};
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2799
|
+
/**
|
|
2800
|
+
* @brief Returns a read-only range over the neighbors of the node referenced
|
|
2801
|
+
* by @p member at the graph @p level.
|
|
2802
|
+
*/
|
|
2803
|
+
neighbors_view_t neighbors(member_citerator_t member, std::size_t level) const noexcept {
|
|
2804
|
+
return neighbors(get_slot(member), level);
|
|
2805
|
+
}
|
|
2806
|
+
|
|
2807
|
+
/**
|
|
2808
|
+
* @brief Returns the top graph level at which the node at @p slot is present.
|
|
2809
|
+
*/
|
|
2810
|
+
std::size_t level_of(compressed_slot_t slot) const noexcept {
|
|
2811
|
+
return static_cast<std::size_t>(static_cast<level_t>(node_at_(slot).level()));
|
|
2812
|
+
}
|
|
2813
|
+
|
|
2814
|
+
/**
|
|
2815
|
+
* @brief Returns the top graph level at which the node referenced by @p member
|
|
2816
|
+
* is present.
|
|
2817
|
+
*/
|
|
2818
|
+
std::size_t level_of(member_citerator_t member) const noexcept { return level_of(get_slot(member)); }
|
|
2819
|
+
|
|
2550
2820
|
dynamic_allocator_t const& dynamic_allocator() const noexcept { return dynamic_allocator_; }
|
|
2551
2821
|
tape_allocator_t const& tape_allocator() const noexcept { return tape_allocator_; }
|
|
2552
2822
|
|
|
@@ -2919,7 +3189,10 @@ class index_gt {
|
|
|
2919
3189
|
return result.failed("Can't add to an immutable index");
|
|
2920
3190
|
|
|
2921
3191
|
// Make sure we have enough local memory to perform this request
|
|
2922
|
-
context_t
|
|
3192
|
+
context_t* context_ptr = context_or_null_(config.thread);
|
|
3193
|
+
if (!context_ptr)
|
|
3194
|
+
return result.failed("Reserve capacity ahead of insertions!");
|
|
3195
|
+
context_t& context = *context_ptr;
|
|
2923
3196
|
top_candidates_t& top = context.top_candidates;
|
|
2924
3197
|
next_candidates_t& next = context.next_candidates;
|
|
2925
3198
|
top.clear();
|
|
@@ -3057,7 +3330,10 @@ class index_gt {
|
|
|
3057
3330
|
compressed_slot_t updated_slot = iterator.slot_;
|
|
3058
3331
|
|
|
3059
3332
|
// Make sure we have enough local memory to perform this request
|
|
3060
|
-
context_t
|
|
3333
|
+
context_t* context_ptr = context_or_null_(config.thread);
|
|
3334
|
+
if (!context_ptr)
|
|
3335
|
+
return result.failed("Reserve capacity ahead of updates!");
|
|
3336
|
+
context_t& context = *context_ptr;
|
|
3061
3337
|
top_candidates_t& top = context.top_candidates;
|
|
3062
3338
|
next_candidates_t& next = context.next_candidates;
|
|
3063
3339
|
top.clear();
|
|
@@ -3425,26 +3701,31 @@ class index_gt {
|
|
|
3425
3701
|
|
|
3426
3702
|
// Progress status
|
|
3427
3703
|
std::size_t processed = 0;
|
|
3428
|
-
|
|
3704
|
+
checked_size_result_t header_size = checked_size_from_u64(header.size);
|
|
3705
|
+
if (!header_size)
|
|
3706
|
+
return result.failed("Index is too large to serialize");
|
|
3707
|
+
checked_size_result_t total = checked_mul(std::size_t{2}, header_size.value);
|
|
3708
|
+
if (!total)
|
|
3709
|
+
return result.failed("Index is too large to serialize");
|
|
3429
3710
|
|
|
3430
3711
|
// Export the number of levels per node
|
|
3431
3712
|
// That is both enough to estimate the overall memory consumption,
|
|
3432
3713
|
// and to be able to estimate the offsets of every entry in the file.
|
|
3433
|
-
for (std::size_t i = 0; i !=
|
|
3714
|
+
for (std::size_t i = 0; i != header_size.value; ++i) {
|
|
3434
3715
|
node_t node = node_at_(i);
|
|
3435
3716
|
level_t level = node.level();
|
|
3436
3717
|
if (!output(&level, sizeof(level)))
|
|
3437
3718
|
return result.failed("Failed to serialize into stream");
|
|
3438
|
-
if (!progress(++processed, total))
|
|
3719
|
+
if (!progress(++processed, total.value))
|
|
3439
3720
|
return result.failed("Terminated by user");
|
|
3440
3721
|
}
|
|
3441
3722
|
|
|
3442
3723
|
// After that dump the nodes themselves
|
|
3443
|
-
for (std::size_t i = 0; i !=
|
|
3724
|
+
for (std::size_t i = 0; i != header_size.value; ++i) {
|
|
3444
3725
|
span_bytes_t node_bytes = node_bytes_(node_at_(i));
|
|
3445
3726
|
if (!output(node_bytes.data(), node_bytes.size()))
|
|
3446
3727
|
return result.failed("Failed to serialize into stream");
|
|
3447
|
-
if (!progress(++processed, total))
|
|
3728
|
+
if (!progress(++processed, total.value))
|
|
3448
3729
|
return result.failed("Terminated by user");
|
|
3449
3730
|
}
|
|
3450
3731
|
|
|
@@ -3476,10 +3757,16 @@ class index_gt {
|
|
|
3476
3757
|
|
|
3477
3758
|
// Allocate some dynamic memory to read all the levels
|
|
3478
3759
|
using levels_allocator_t = typename dynamic_allocator_traits_t::template rebind_alloc<level_t>;
|
|
3479
|
-
|
|
3760
|
+
checked_size_result_t header_size = checked_size_from_u64(header.size);
|
|
3761
|
+
if (!header_size)
|
|
3762
|
+
return result.failed("Index is too large");
|
|
3763
|
+
buffer_gt<level_t, levels_allocator_t> levels(header_size.value);
|
|
3480
3764
|
if (!levels)
|
|
3481
3765
|
return result.failed("Out of memory");
|
|
3482
|
-
|
|
3766
|
+
checked_size_result_t levels_bytes = checked_mul(header_size.value, sizeof(level_t));
|
|
3767
|
+
if (!levels_bytes)
|
|
3768
|
+
return result.failed("Index is too large");
|
|
3769
|
+
if (!input(levels, levels_bytes.value))
|
|
3483
3770
|
return result.failed("Failed to pull nodes levels from the stream");
|
|
3484
3771
|
|
|
3485
3772
|
// Submit metadata
|
|
@@ -3491,26 +3778,26 @@ class index_gt {
|
|
|
3491
3778
|
|
|
3492
3779
|
pre_ = precompute_(config_);
|
|
3493
3780
|
index_limits_t limits;
|
|
3494
|
-
limits.members =
|
|
3781
|
+
limits.members = header_size.value;
|
|
3495
3782
|
limits.threads_add = (std::max<std::size_t>)(1, old_limits.threads_add);
|
|
3496
3783
|
limits.threads_search = (std::max<std::size_t>)(1, old_limits.threads_search);
|
|
3497
3784
|
if (!reserve(limits)) {
|
|
3498
3785
|
reset();
|
|
3499
3786
|
return result.failed("Out of memory");
|
|
3500
3787
|
}
|
|
3501
|
-
nodes_count_ =
|
|
3788
|
+
nodes_count_ = header_size.value;
|
|
3502
3789
|
max_level_ = static_cast<level_t>(header.max_level);
|
|
3503
3790
|
entry_slot_ = static_cast<compressed_slot_t>(header.entry_slot);
|
|
3504
3791
|
|
|
3505
3792
|
// Load the nodes
|
|
3506
|
-
for (std::size_t i = 0; i !=
|
|
3793
|
+
for (std::size_t i = 0; i != header_size.value; ++i) {
|
|
3507
3794
|
span_bytes_t node_bytes = node_malloc_(levels[i]);
|
|
3508
3795
|
if (!input(node_bytes.data(), node_bytes.size())) {
|
|
3509
3796
|
reset();
|
|
3510
3797
|
return result.failed("Failed to pull nodes from the stream");
|
|
3511
3798
|
}
|
|
3512
3799
|
nodes_[i] = node_t{node_bytes.data()};
|
|
3513
|
-
if (!progress(i + 1,
|
|
3800
|
+
if (!progress(i + 1, header_size.value))
|
|
3514
3801
|
return result.failed("Terminated by user");
|
|
3515
3802
|
}
|
|
3516
3803
|
return {};
|
|
@@ -3657,11 +3944,14 @@ class index_gt {
|
|
|
3657
3944
|
reset();
|
|
3658
3945
|
return result;
|
|
3659
3946
|
}
|
|
3947
|
+
checked_size_result_t header_size = checked_size_from_u64(header.size);
|
|
3948
|
+
if (!header_size)
|
|
3949
|
+
return result.failed("Index is too large");
|
|
3660
3950
|
|
|
3661
3951
|
// Precompute offsets of every node, but before that we need to update the configs
|
|
3662
3952
|
// This could have been done with `std::exclusive_scan`, but it's only available from C++17.
|
|
3663
3953
|
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(
|
|
3954
|
+
buffer_gt<std::size_t, offsets_allocator_t> offsets(header_size.value);
|
|
3665
3955
|
if (!offsets)
|
|
3666
3956
|
return result.failed("Out of memory");
|
|
3667
3957
|
|
|
@@ -3673,33 +3963,47 @@ class index_gt {
|
|
|
3673
3963
|
|
|
3674
3964
|
pre_ = precompute_(config_);
|
|
3675
3965
|
misaligned_ptr_gt<level_t> levels{(byte_t*)file.data() + offset + sizeof(header)};
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3966
|
+
checked_size_result_t levels_bytes = checked_mul(sizeof(level_t), header_size.value);
|
|
3967
|
+
checked_size_result_t offset_after_header = checked_add(offset, sizeof(header));
|
|
3968
|
+
checked_size_result_t first_offset = levels_bytes && offset_after_header
|
|
3969
|
+
? checked_add(offset_after_header.value, levels_bytes.value)
|
|
3970
|
+
: checked_size_overflow();
|
|
3971
|
+
if (!first_offset)
|
|
3972
|
+
return result.failed("Index is too large");
|
|
3973
|
+
offsets[0u] = first_offset.value;
|
|
3974
|
+
for (std::size_t i = 1; i < header_size.value; ++i) {
|
|
3975
|
+
checked_size_result_t next_offset = checked_add(offsets[i - 1], node_bytes_(levels[i - 1]));
|
|
3976
|
+
if (!next_offset)
|
|
3977
|
+
return result.failed("Index is too large");
|
|
3978
|
+
offsets[i] = next_offset.value;
|
|
3979
|
+
}
|
|
3679
3980
|
|
|
3680
|
-
|
|
3681
|
-
|
|
3981
|
+
checked_size_result_t total_bytes =
|
|
3982
|
+
checked_add(offsets[header_size.value - 1], node_bytes_(levels[header_size.value - 1]));
|
|
3983
|
+
if (!total_bytes)
|
|
3984
|
+
return result.failed("Index is too large");
|
|
3985
|
+
if (file.size() < total_bytes.value) {
|
|
3682
3986
|
reset();
|
|
3683
3987
|
return result.failed("File is corrupted and can't fit all the nodes");
|
|
3684
3988
|
}
|
|
3685
3989
|
|
|
3686
3990
|
// Submit metadata and reserve memory
|
|
3687
3991
|
index_limits_t limits;
|
|
3688
|
-
limits.members =
|
|
3992
|
+
limits.members = header_size.value;
|
|
3689
3993
|
limits.threads_add = (std::max<std::size_t>)(1, old_limits.threads_add);
|
|
3690
3994
|
limits.threads_search = (std::max<std::size_t>)(1, old_limits.threads_search);
|
|
3691
3995
|
if (!reserve(limits)) {
|
|
3692
3996
|
reset();
|
|
3693
3997
|
return result.failed("Out of memory");
|
|
3694
3998
|
}
|
|
3695
|
-
nodes_count_ =
|
|
3999
|
+
nodes_count_ = header_size.value;
|
|
3696
4000
|
max_level_ = static_cast<level_t>(header.max_level);
|
|
3697
4001
|
entry_slot_ = static_cast<compressed_slot_t>(header.entry_slot);
|
|
3698
4002
|
|
|
3699
4003
|
// Rapidly address all the nodes
|
|
3700
|
-
for (std::size_t i = 0; i !=
|
|
4004
|
+
for (std::size_t i = 0; i != header_size.value; ++i) {
|
|
3701
4005
|
nodes_[i] = node_t{(byte_t*)file.data() + offsets[i]};
|
|
3702
|
-
if (!progress(i + 1,
|
|
4006
|
+
if (!progress(i + 1, header_size.value))
|
|
3703
4007
|
return result.failed("Terminated by user");
|
|
3704
4008
|
}
|
|
3705
4009
|
viewed_file_ = std::move(file);
|
|
@@ -3751,7 +4055,9 @@ class index_gt {
|
|
|
3751
4055
|
// Progress status
|
|
3752
4056
|
std::atomic<bool> do_tasks{true};
|
|
3753
4057
|
std::atomic<std::size_t> processed{0};
|
|
3754
|
-
|
|
4058
|
+
checked_size_result_t total = checked_mul(std::size_t{3}, slots_and_levels.size());
|
|
4059
|
+
if (!total)
|
|
4060
|
+
return;
|
|
3755
4061
|
|
|
3756
4062
|
// For every bottom level node, determine its parent cluster
|
|
3757
4063
|
executor.dynamic(slots_and_levels.size(), [&](std::size_t thread_idx, std::size_t old_slot_as_uint) {
|
|
@@ -3764,7 +4070,7 @@ class index_gt {
|
|
|
3764
4070
|
slots_and_levels[old_slot] = {old_slot, cluster, node_at_(old_slot).level()};
|
|
3765
4071
|
++processed;
|
|
3766
4072
|
if (thread_idx == 0)
|
|
3767
|
-
do_tasks = progress(processed.load(), total);
|
|
4073
|
+
do_tasks = progress(processed.load(), total.value);
|
|
3768
4074
|
return do_tasks.load();
|
|
3769
4075
|
});
|
|
3770
4076
|
if (!do_tasks.load())
|
|
@@ -3798,7 +4104,7 @@ class index_gt {
|
|
|
3798
4104
|
neighbor = static_cast<compressed_slot_t>(old_slot_to_new[compressed_slot_t(neighbor)]);
|
|
3799
4105
|
|
|
3800
4106
|
reordered_nodes[new_slot] = new_node;
|
|
3801
|
-
if (!progress(++processed, total))
|
|
4107
|
+
if (!progress(++processed, total.value))
|
|
3802
4108
|
return;
|
|
3803
4109
|
}
|
|
3804
4110
|
|
|
@@ -3807,7 +4113,7 @@ class index_gt {
|
|
|
3807
4113
|
slot_transition(node_at_(old_slot).ckey(), //
|
|
3808
4114
|
static_cast<compressed_slot_t>(old_slot), //
|
|
3809
4115
|
static_cast<compressed_slot_t>(new_slot));
|
|
3810
|
-
if (!progress(++processed, total))
|
|
4116
|
+
if (!progress(++processed, total.value))
|
|
3811
4117
|
return;
|
|
3812
4118
|
}
|
|
3813
4119
|
|