usearch 2.6.1 → 2.7.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/. npmignore +14 -0
- package/.cmake-format.py +19 -0
- package/.github/workflows/Dockerfile.libs +3 -3
- package/.github/workflows/prerelease.yml +85 -64
- package/.github/workflows/release.yml +18 -16
- package/.gitignore +1 -1
- package/.vscode/launch.json +47 -14
- package/.vscode/settings.json +7 -0
- package/.vscode/tasks.json +44 -8
- package/CITATION.cff +1 -1
- package/CMakeLists.txt +218 -67
- package/Cargo.toml +1 -1
- package/README.md +1 -1
- package/binding.gyp +3 -2
- package/build.gradle +19 -10
- package/c/CMakeLists.txt +21 -48
- package/c/README.md +5 -5
- package/c/lib.cpp +52 -7
- package/c/test.c +137 -94
- package/c/usearch.h +42 -0
- package/cmake/clipp.cmake +0 -1
- package/cmake/jemalloc.cmake +21 -37
- package/conanfile.py +1 -1
- package/cpp/CMakeLists.txt +14 -123
- package/cpp/bench.cpp +0 -12
- package/cpp/test.cpp +29 -1
- package/csharp/nuget/nuget-package.props +1 -1
- package/docs/compilation.md +23 -16
- package/docs/conf.py +1 -1
- package/golang/README.md +52 -51
- package/golang/lib.go +1 -1
- package/golang/lib_test.go +4 -1
- package/include/usearch/index.hpp +2 -2
- package/include/usearch/index_plugins.hpp +345 -160
- package/javascript/lib.cpp +204 -250
- package/javascript/usearch.js +440 -2
- package/javascript/usearch.test.js +37 -0
- package/package.json +10 -5
- package/python/lib.cpp +96 -225
- package/python/scripts/bench_exact.py +2 -2
- package/python/scripts/test_jit.py +2 -13
- package/python/scripts/test_sparse.py +1 -1
- package/python/scripts/test_tooling.py +26 -5
- package/rust/lib.cpp +12 -4
- package/rust/lib.hpp +7 -0
- package/rust/lib.rs +170 -10
- package/setup.py +14 -4
- package/wasm/CMakeLists.txt +4 -12
- package/wasm/lib.cpp +1 -1
- package/wasmer.toml +1 -1
- package/javascript/docs.js +0 -111
- package/javascript/test.js +0 -32
- package/javascript/usearch.d.ts +0 -110
- package/tsconfig.json +0 -8
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
#pragma once
|
|
2
|
+
#define __STDC_WANT_IEC_60559_TYPES_EXT__
|
|
3
|
+
#include <float.h> // `_Float16`
|
|
2
4
|
#include <stdlib.h> // `aligned_alloc`
|
|
3
5
|
|
|
4
6
|
#include <cstring> // `std::strncmp`
|
|
@@ -19,20 +21,18 @@
|
|
|
19
21
|
#include <sys/auxv.h> // `getauxval()`
|
|
20
22
|
#endif
|
|
21
23
|
|
|
22
|
-
#if defined(USEARCH_DEFINED_ARM)
|
|
23
|
-
#include <arm_fp16.h> // `__fp16`
|
|
24
|
-
#endif
|
|
25
|
-
|
|
26
24
|
#if !defined(USEARCH_USE_NATIVE_F16)
|
|
27
25
|
#if defined(__AVX512F__)
|
|
28
26
|
#define USEARCH_USE_NATIVE_F16 1
|
|
29
27
|
#elif defined(USEARCH_DEFINED_ARM)
|
|
28
|
+
#include <arm_fp16.h> // `__fp16`
|
|
30
29
|
#define USEARCH_USE_NATIVE_F16 1
|
|
31
30
|
#else
|
|
32
31
|
#define USEARCH_USE_NATIVE_F16 0
|
|
33
|
-
#include <fp16/fp16.h>
|
|
34
32
|
#endif
|
|
35
|
-
#
|
|
33
|
+
#endif
|
|
34
|
+
|
|
35
|
+
#if !USEARCH_USE_NATIVE_F16
|
|
36
36
|
#include <fp16/fp16.h>
|
|
37
37
|
#endif
|
|
38
38
|
|
|
@@ -346,6 +346,10 @@ inline expected_gt<scalar_kind_t> scalar_kind_from_name(char const* name, std::s
|
|
|
346
346
|
return parsed;
|
|
347
347
|
}
|
|
348
348
|
|
|
349
|
+
inline expected_gt<scalar_kind_t> scalar_kind_from_name(char const* name) {
|
|
350
|
+
return scalar_kind_from_name(name, std::strlen(name));
|
|
351
|
+
}
|
|
352
|
+
|
|
349
353
|
inline expected_gt<metric_kind_t> metric_from_name(char const* name, std::size_t len) {
|
|
350
354
|
expected_gt<metric_kind_t> parsed;
|
|
351
355
|
if (str_equals(name, len, "l2sq") || str_equals(name, len, "euclidean_sq")) {
|
|
@@ -370,6 +374,10 @@ inline expected_gt<metric_kind_t> metric_from_name(char const* name, std::size_t
|
|
|
370
374
|
return parsed;
|
|
371
375
|
}
|
|
372
376
|
|
|
377
|
+
inline expected_gt<metric_kind_t> metric_from_name(char const* name) {
|
|
378
|
+
return metric_from_name(name, std::strlen(name));
|
|
379
|
+
}
|
|
380
|
+
|
|
373
381
|
inline float f16_to_f32(std::uint16_t u16) noexcept {
|
|
374
382
|
#if USEARCH_USE_NATIVE_F16
|
|
375
383
|
f16_native_t f16;
|
|
@@ -1330,17 +1338,31 @@ using distance_punned_t = float;
|
|
|
1330
1338
|
using span_punned_t = span_gt<byte_t const>;
|
|
1331
1339
|
|
|
1332
1340
|
/**
|
|
1333
|
-
* @brief
|
|
1334
|
-
*
|
|
1341
|
+
* @brief The signature of the user-defined function.
|
|
1342
|
+
* Can be just two array pointers, precompiled for a specific array length,
|
|
1343
|
+
* or include one or two array sizes as 64-bit unsigned integers.
|
|
1344
|
+
*/
|
|
1345
|
+
enum class metric_punned_signature_t {
|
|
1346
|
+
array_array_k = 0,
|
|
1347
|
+
array_array_size_k,
|
|
1348
|
+
};
|
|
1349
|
+
|
|
1350
|
+
/**
|
|
1351
|
+
* @brief Type-punned metric class, which unlike STL's `std::function` avoids any memory allocations.
|
|
1352
|
+
* It also provides additional APIs to check, if SIMD hardware-acceleration is available.
|
|
1335
1353
|
*/
|
|
1336
1354
|
class metric_punned_t {
|
|
1337
1355
|
public:
|
|
1338
1356
|
using scalar_t = byte_t;
|
|
1339
1357
|
using result_t = distance_punned_t;
|
|
1340
|
-
using stl_function_t = std::function<float(byte_t const*, byte_t const*)>;
|
|
1341
1358
|
|
|
1342
1359
|
private:
|
|
1343
|
-
|
|
1360
|
+
using punned_arg_t = std::size_t;
|
|
1361
|
+
using raw_ptr_t = result_t (*)(punned_arg_t, punned_arg_t, punned_arg_t, punned_arg_t);
|
|
1362
|
+
raw_ptr_t raw_ptr_ = nullptr;
|
|
1363
|
+
punned_arg_t raw_arg3_ = 0;
|
|
1364
|
+
punned_arg_t raw_arg4_ = 0;
|
|
1365
|
+
|
|
1344
1366
|
std::size_t dimensions_ = 0;
|
|
1345
1367
|
metric_kind_t metric_kind_ = metric_kind_t::unknown_k;
|
|
1346
1368
|
scalar_kind_t scalar_kind_ = scalar_kind_t::unknown_k;
|
|
@@ -1349,32 +1371,41 @@ class metric_punned_t {
|
|
|
1349
1371
|
public:
|
|
1350
1372
|
/**
|
|
1351
1373
|
* @brief Computes the distance between two vectors of fixed length.
|
|
1352
|
-
*
|
|
1374
|
+
*
|
|
1375
|
+
* ! This is the only relevant function in the object. Everything else is just dynamic dispatch logic.
|
|
1353
1376
|
*/
|
|
1354
|
-
inline result_t operator()(byte_t const* a, byte_t const* b) const noexcept {
|
|
1377
|
+
inline result_t operator()(byte_t const* a, byte_t const* b) const noexcept {
|
|
1378
|
+
return raw_ptr_(reinterpret_cast<punned_arg_t>(a), reinterpret_cast<punned_arg_t>(b), raw_arg3_, raw_arg4_);
|
|
1379
|
+
}
|
|
1355
1380
|
|
|
1356
|
-
inline metric_punned_t() = default;
|
|
1357
|
-
inline metric_punned_t(metric_punned_t const&) = default;
|
|
1358
|
-
inline metric_punned_t& operator=(metric_punned_t const&) = default;
|
|
1381
|
+
inline metric_punned_t() noexcept = default;
|
|
1382
|
+
inline metric_punned_t(metric_punned_t const&) noexcept = default;
|
|
1383
|
+
inline metric_punned_t& operator=(metric_punned_t const&) noexcept = default;
|
|
1384
|
+
inline metric_punned_t( //
|
|
1385
|
+
std::size_t dimensions, //
|
|
1386
|
+
metric_kind_t metric_kind = metric_kind_t::l2sq_k, //
|
|
1387
|
+
scalar_kind_t scalar_kind = scalar_kind_t::f32_k) noexcept
|
|
1388
|
+
: raw_arg3_(dimensions), raw_arg4_(dimensions), dimensions_(dimensions), metric_kind_(metric_kind),
|
|
1389
|
+
scalar_kind_(scalar_kind), isa_kind_(isa_kind_t::auto_k) {
|
|
1390
|
+
reset_();
|
|
1391
|
+
}
|
|
1359
1392
|
|
|
1360
|
-
inline metric_punned_t(
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1393
|
+
inline metric_punned_t( //
|
|
1394
|
+
std::size_t dimensions, //
|
|
1395
|
+
std::uintptr_t metric_uintptr, metric_punned_signature_t signature, //
|
|
1396
|
+
metric_kind_t metric_kind, //
|
|
1397
|
+
scalar_kind_t scalar_kind) noexcept
|
|
1398
|
+
: raw_ptr_(reinterpret_cast<raw_ptr_t>(metric_uintptr)), dimensions_(dimensions), metric_kind_(metric_kind),
|
|
1399
|
+
scalar_kind_(scalar_kind) {
|
|
1365
1400
|
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
scalar_kind_t scalar_kind = scalar_kind_t::f32_k) {
|
|
1369
|
-
std::size_t bytes_per_vector = divide_round_up<CHAR_BIT>(dimensions * bits_per_scalar(scalar_kind));
|
|
1370
|
-
*this = make_(bytes_per_vector, metric_kind, scalar_kind);
|
|
1371
|
-
dimensions_ = dimensions;
|
|
1401
|
+
// We don't need to explicitly parse signature, as all of them are compatible.
|
|
1402
|
+
(void)signature;
|
|
1372
1403
|
}
|
|
1373
1404
|
|
|
1374
|
-
std::size_t dimensions() const noexcept { return dimensions_; }
|
|
1375
|
-
metric_kind_t metric_kind() const noexcept { return metric_kind_; }
|
|
1376
|
-
scalar_kind_t scalar_kind() const noexcept { return scalar_kind_; }
|
|
1377
|
-
isa_kind_t isa_kind() const noexcept { return isa_kind_; }
|
|
1405
|
+
inline std::size_t dimensions() const noexcept { return dimensions_; }
|
|
1406
|
+
inline metric_kind_t metric_kind() const noexcept { return metric_kind_; }
|
|
1407
|
+
inline scalar_kind_t scalar_kind() const noexcept { return scalar_kind_; }
|
|
1408
|
+
inline isa_kind_t isa_kind() const noexcept { return isa_kind_; }
|
|
1378
1409
|
|
|
1379
1410
|
inline std::size_t bytes_per_vector() const noexcept {
|
|
1380
1411
|
return divide_round_up<CHAR_BIT>(dimensions_ * bits_per_scalar(scalar_kind_));
|
|
@@ -1385,172 +1416,309 @@ class metric_punned_t {
|
|
|
1385
1416
|
}
|
|
1386
1417
|
|
|
1387
1418
|
private:
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
case metric_kind_t::
|
|
1392
|
-
case metric_kind_t::
|
|
1393
|
-
case metric_kind_t::
|
|
1394
|
-
case metric_kind_t::
|
|
1395
|
-
case metric_kind_t::haversine_k: return haversine_metric_(scalar_kind);
|
|
1396
|
-
|
|
1397
|
-
case metric_kind_t::hamming_k:
|
|
1398
|
-
return {to_stl_<metric_hamming_gt<b1x8_t>>(bytes_per_vector), bytes_per_vector, metric_kind_t::hamming_k,
|
|
1399
|
-
scalar_kind_t::b1x8_k, isa_kind_t::auto_k};
|
|
1400
|
-
|
|
1419
|
+
void reset_() noexcept {
|
|
1420
|
+
switch (metric_kind_) {
|
|
1421
|
+
case metric_kind_t::ip_k: return reset_ip_metric_();
|
|
1422
|
+
case metric_kind_t::cos_k: return reset_cos_metric_();
|
|
1423
|
+
case metric_kind_t::l2sq_k: return reset_l2sq_metric_();
|
|
1424
|
+
case metric_kind_t::pearson_k: return reset_pearson_metric_();
|
|
1425
|
+
case metric_kind_t::haversine_k: return reset_haversine_metric_();
|
|
1401
1426
|
case metric_kind_t::jaccard_k: // Equivalent to Tanimoto
|
|
1402
1427
|
case metric_kind_t::tanimoto_k:
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1428
|
+
raw_ptr_ = &equidimensional_<metric_tanimoto_gt<b1x8_t>>,
|
|
1429
|
+
raw_arg3_ = raw_arg4_ = divide_round_up<CHAR_BIT>(dimensions_), scalar_kind_ = scalar_kind_t::b1x8_k;
|
|
1430
|
+
break;
|
|
1431
|
+
case metric_kind_t::hamming_k:
|
|
1432
|
+
raw_ptr_ = &equidimensional_<metric_hamming_gt<b1x8_t>>,
|
|
1433
|
+
raw_arg3_ = raw_arg4_ = divide_round_up<CHAR_BIT>(dimensions_), scalar_kind_ = scalar_kind_t::b1x8_k;
|
|
1434
|
+
break;
|
|
1406
1435
|
case metric_kind_t::sorensen_k:
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
default: return
|
|
1436
|
+
raw_ptr_ = &equidimensional_<metric_sorensen_gt<b1x8_t>>,
|
|
1437
|
+
raw_arg3_ = raw_arg4_ = divide_round_up<CHAR_BIT>(dimensions_), scalar_kind_ = scalar_kind_t::b1x8_k;
|
|
1438
|
+
break;
|
|
1439
|
+
default: return;
|
|
1411
1440
|
}
|
|
1412
1441
|
}
|
|
1413
1442
|
|
|
1414
|
-
template <typename typed_at>
|
|
1443
|
+
template <typename typed_at>
|
|
1444
|
+
inline static result_t equidimensional_( //
|
|
1445
|
+
punned_arg_t a, punned_arg_t b, //
|
|
1446
|
+
punned_arg_t a_dimensions, punned_arg_t b_dimensions) noexcept {
|
|
1415
1447
|
using scalar_t = typename typed_at::scalar_t;
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
};
|
|
1448
|
+
(void)b_dimensions;
|
|
1449
|
+
return typed_at{}((scalar_t const*)a, (scalar_t const*)b, a_dimensions);
|
|
1419
1450
|
}
|
|
1420
1451
|
|
|
1421
1452
|
template <typename scalar_at>
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1453
|
+
bool reset_if_(isa_kind_t isa, result_t (*function_ptr)(scalar_at, scalar_at, size_t),
|
|
1454
|
+
bool second_condition = true) noexcept {
|
|
1455
|
+
raw_ptr_t punned_ptr = reinterpret_cast<raw_ptr_t>(function_ptr);
|
|
1456
|
+
if (hardware_supports(isa) && second_condition) {
|
|
1457
|
+
raw_ptr_ = punned_ptr;
|
|
1458
|
+
isa_kind_ = isa;
|
|
1459
|
+
return true;
|
|
1460
|
+
} else
|
|
1461
|
+
return false;
|
|
1426
1462
|
}
|
|
1427
1463
|
|
|
1428
1464
|
// clang-format off
|
|
1429
|
-
|
|
1465
|
+
void reset_ip_metric_f32_() noexcept {
|
|
1430
1466
|
#if USEARCH_USE_SIMSIMD
|
|
1431
1467
|
#if SIMSIMD_TARGET_ARM_NEON
|
|
1432
|
-
if (
|
|
1433
|
-
#
|
|
1434
|
-
|
|
1435
|
-
|
|
1468
|
+
if (reset_if_(isa_kind_t::neon_k, &simsimd_dot_f32x4_neon, dimensions_ % 4 == 0)) return;
|
|
1469
|
+
#elif SIMSIMD_TARGET_ARM_SVE
|
|
1470
|
+
if (reset_if_(isa_kind_t::sve_k, &simsimd_dot_f32_sve)) return;
|
|
1471
|
+
#elif SIMSIMD_TARGET_X86_AVX2
|
|
1472
|
+
if (reset_if_(isa_kind_t::avx2_k, &simsimd_dot_f32x4_avx2, dimensions_ % 4 == 0)) return;
|
|
1436
1473
|
#endif
|
|
1437
|
-
#if SIMSIMD_TARGET_X86_AVX2
|
|
1438
|
-
if (hardware_supports(isa_kind_t::avx2_k) && bytes_per_vector % 16 == 0) return {pun_stl_<f32_t>([=](f32_t const* a, f32_t const* b) { return simsimd_dot_f32x4_avx2(a, b, bytes_per_vector / 4); }), bytes_per_vector, metric_kind_t::ip_k, scalar_kind_t::f32_k, isa_kind_t::avx2_k};
|
|
1439
1474
|
#endif
|
|
1440
|
-
|
|
1441
|
-
return {to_stl_<metric_ip_gt<f32_t>>(bytes_per_vector), bytes_per_vector, metric_kind_t::ip_k, scalar_kind_t::f32_k, isa_kind_t::auto_k};
|
|
1475
|
+
raw_ptr_ = &equidimensional_<metric_ip_gt<f32_t>>;
|
|
1442
1476
|
}
|
|
1443
1477
|
|
|
1444
|
-
|
|
1478
|
+
void reset_cos_metric_f16_() noexcept {
|
|
1445
1479
|
#if USEARCH_USE_SIMSIMD
|
|
1446
1480
|
#if SIMSIMD_TARGET_X86_AVX512
|
|
1447
|
-
if (
|
|
1448
|
-
#
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
#
|
|
1453
|
-
if (
|
|
1454
|
-
#endif
|
|
1455
|
-
#if SIMSIMD_TARGET_ARM_SVE
|
|
1456
|
-
if (hardware_supports(isa_kind_t::sve_k)) return {pun_stl_<simsimd_f16_t>([=](simsimd_f16_t const* a, simsimd_f16_t const* b) { return simsimd_cos_f16_sve(a, b, bytes_per_vector / 4); }), bytes_per_vector, metric_kind_t::cos_k, scalar_kind_t::f16_k, isa_kind_t::sve_k};
|
|
1481
|
+
if (reset_if_(isa_kind_t::avx512f16_k, &simsimd_cos_f16x16_avx512, dimensions_ % 16 == 0)) return;
|
|
1482
|
+
#elif SIMSIMD_TARGET_X86_AVX2
|
|
1483
|
+
if (reset_if_(isa_kind_t::avx2f16_k, &simsimd_cos_f16x8_avx2, dimensions_ % 8 == 0)) return;
|
|
1484
|
+
#elif SIMSIMD_TARGET_ARM_NEON
|
|
1485
|
+
if (reset_if_(isa_kind_t::neon_k, &simsimd_cos_f16x4_neon, dimensions_ % 4 == 0)) return;
|
|
1486
|
+
#elif SIMSIMD_TARGET_ARM_SVE
|
|
1487
|
+
if (reset_if_(isa_kind_t::sve_k, &simsimd_cos_f16_sve)) return;
|
|
1457
1488
|
#endif
|
|
1458
1489
|
#endif
|
|
1459
|
-
|
|
1490
|
+
raw_ptr_ = &equidimensional_<metric_cos_gt<f16_t, f32_t>>;
|
|
1460
1491
|
}
|
|
1461
1492
|
|
|
1462
|
-
|
|
1493
|
+
void reset_ip_metric_f16_() noexcept {
|
|
1463
1494
|
#if USEARCH_USE_SIMSIMD
|
|
1464
1495
|
#if SIMSIMD_TARGET_X86_AVX512
|
|
1465
|
-
if (
|
|
1496
|
+
if (reset_if_(isa_kind_t::avx512f16_k, &simsimd_dot_f16x16_avx512, dimensions_ % 16 == 0)) return;
|
|
1497
|
+
#elif SIMSIMD_TARGET_X86_AVX2
|
|
1498
|
+
if (reset_if_(isa_kind_t::avx2f16_k, &simsimd_dot_f16x8_avx2, dimensions_ % 8 == 0)) return;
|
|
1499
|
+
#elif SIMSIMD_TARGET_ARM_NEON
|
|
1500
|
+
if (reset_if_(isa_kind_t::neon_k, &simsimd_dot_f16x4_neon, dimensions_ % 4 == 0)) return;
|
|
1466
1501
|
#endif
|
|
1467
|
-
#if SIMSIMD_TARGET_X86_AVX2
|
|
1468
|
-
if (hardware_supports(isa_kind_t::avx2f16_k) && bytes_per_vector % 16 == 0) return {pun_stl_<simsimd_f16_t>([=](simsimd_f16_t const* a, simsimd_f16_t const* b) { return simsimd_dot_f16x8_avx2(a, b, bytes_per_vector / 2); }), bytes_per_vector, metric_kind_t::ip_k, scalar_kind_t::f16_k, isa_kind_t::avx2f16_k};
|
|
1469
1502
|
#endif
|
|
1470
|
-
|
|
1471
|
-
if (hardware_supports(isa_kind_t::neon_k) && bytes_per_vector % 8 == 0) return {pun_stl_<simsimd_f16_t>([=](simsimd_f16_t const* a, simsimd_f16_t const* b) { return simsimd_dot_f16x4_neon(a, b, bytes_per_vector / 2); }), bytes_per_vector, metric_kind_t::ip_k, scalar_kind_t::f16_k, isa_kind_t::neon_k};
|
|
1472
|
-
#endif
|
|
1473
|
-
#endif
|
|
1474
|
-
return {to_stl_<metric_cos_gt<f16_t, f32_t>>(bytes_per_vector), bytes_per_vector, metric_kind_t::ip_k, scalar_kind_t::f16_k, isa_kind_t::auto_k};
|
|
1503
|
+
raw_ptr_ = &equidimensional_<metric_cos_gt<f16_t, f32_t>>;
|
|
1475
1504
|
}
|
|
1476
1505
|
|
|
1477
|
-
|
|
1506
|
+
void reset_l2sq_metric_f16_() noexcept {
|
|
1478
1507
|
#if USEARCH_USE_SIMSIMD
|
|
1479
1508
|
#if SIMSIMD_TARGET_X86_AVX512
|
|
1480
|
-
if (
|
|
1481
|
-
#
|
|
1482
|
-
|
|
1483
|
-
|
|
1509
|
+
if (reset_if_(isa_kind_t::avx512f16_k, &simsimd_l2sq_f16x16_avx512, dimensions_ % 16 == 0)) return;
|
|
1510
|
+
#elif SIMSIMD_TARGET_X86_AVX2
|
|
1511
|
+
if (reset_if_(isa_kind_t::avx2f16_k, &simsimd_l2sq_f16x8_avx2, dimensions_ % 8 == 0)) return;
|
|
1512
|
+
#elif SIMSIMD_TARGET_ARM_SVE
|
|
1513
|
+
if (reset_if_(isa_kind_t::sve_k, &simsimd_l2sq_f16_sve)) return;
|
|
1484
1514
|
#endif
|
|
1485
|
-
#if SIMSIMD_TARGET_ARM_SVE
|
|
1486
|
-
if (hardware_supports(isa_kind_t::sve_k)) return {pun_stl_<simsimd_f16_t>([=](simsimd_f16_t const* a, simsimd_f16_t const* b) { return simsimd_l2sq_f16_sve(a, b, bytes_per_vector / 4); }), bytes_per_vector, metric_kind_t::l2sq_k, scalar_kind_t::f16_k, isa_kind_t::sve_k};
|
|
1487
1515
|
#endif
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
}
|
|
1516
|
+
raw_ptr_ = &equidimensional_<metric_l2sq_gt<f16_t, f32_t>>;
|
|
1517
|
+
}
|
|
1491
1518
|
|
|
1492
|
-
|
|
1519
|
+
void reset_cos_metric_i8_() noexcept {
|
|
1493
1520
|
#if USEARCH_USE_SIMSIMD
|
|
1494
1521
|
#if SIMSIMD_TARGET_ARM_NEON
|
|
1495
|
-
if (
|
|
1496
|
-
#
|
|
1497
|
-
|
|
1498
|
-
if (hardware_supports(isa_kind_t::avx2_k) && bytes_per_vector % 32 == 0) return {pun_stl_<int8_t>([=](int8_t const* a, int8_t const* b) { return simsimd_cos_i8x32_avx2(a, b, bytes_per_vector); }), bytes_per_vector, metric_kind_t::cos_k, scalar_kind_t::i8_k, isa_kind_t::avx2_k};
|
|
1522
|
+
if (reset_if_(isa_kind_t::neon_k, &simsimd_cos_i8x16_neon, dimensions_ % 16 == 0)) return;
|
|
1523
|
+
#elif SIMSIMD_TARGET_X86_AVX2
|
|
1524
|
+
if (reset_if_(isa_kind_t::avx2_k, &simsimd_cos_i8x32_avx2, dimensions_ % 32 == 0)) return;
|
|
1499
1525
|
#endif
|
|
1500
1526
|
#endif
|
|
1501
|
-
|
|
1527
|
+
raw_ptr_ = &equidimensional_<cos_i8_t>;
|
|
1502
1528
|
}
|
|
1529
|
+
// clang-format on
|
|
1503
1530
|
|
|
1504
|
-
|
|
1505
|
-
switch (
|
|
1506
|
-
case scalar_kind_t::f32_k: return
|
|
1507
|
-
case scalar_kind_t::f16_k: return
|
|
1508
|
-
case scalar_kind_t::i8_k:
|
|
1509
|
-
case scalar_kind_t::f64_k:
|
|
1510
|
-
default: return
|
|
1531
|
+
void reset_ip_metric_() noexcept {
|
|
1532
|
+
switch (scalar_kind_) { // The two most common numeric types for the most common metric have optimized versions
|
|
1533
|
+
case scalar_kind_t::f32_k: return reset_ip_metric_f32_();
|
|
1534
|
+
case scalar_kind_t::f16_k: return reset_ip_metric_f16_();
|
|
1535
|
+
case scalar_kind_t::i8_k: return reset_cos_metric_i8_();
|
|
1536
|
+
case scalar_kind_t::f64_k: raw_ptr_ = &equidimensional_<metric_ip_gt<f64_t>>; break;
|
|
1537
|
+
default: return;
|
|
1511
1538
|
}
|
|
1512
1539
|
}
|
|
1513
1540
|
|
|
1514
|
-
|
|
1515
|
-
switch (
|
|
1516
|
-
case scalar_kind_t::i8_k:
|
|
1517
|
-
case scalar_kind_t::f16_k: return
|
|
1518
|
-
case scalar_kind_t::f32_k:
|
|
1519
|
-
case scalar_kind_t::f64_k:
|
|
1520
|
-
default: return
|
|
1541
|
+
void reset_l2sq_metric_() noexcept {
|
|
1542
|
+
switch (scalar_kind_) {
|
|
1543
|
+
case scalar_kind_t::i8_k: raw_ptr_ = &equidimensional_<l2sq_i8_t>; break;
|
|
1544
|
+
case scalar_kind_t::f16_k: return reset_l2sq_metric_f16_();
|
|
1545
|
+
case scalar_kind_t::f32_k: raw_ptr_ = &equidimensional_<metric_l2sq_gt<f32_t>>; break;
|
|
1546
|
+
case scalar_kind_t::f64_k: raw_ptr_ = &equidimensional_<metric_l2sq_gt<f64_t>>; break;
|
|
1547
|
+
default: return;
|
|
1521
1548
|
}
|
|
1522
1549
|
}
|
|
1523
1550
|
|
|
1524
|
-
|
|
1525
|
-
switch (
|
|
1526
|
-
case scalar_kind_t::i8_k: return
|
|
1527
|
-
case scalar_kind_t::f16_k: return
|
|
1528
|
-
case scalar_kind_t::f32_k:
|
|
1529
|
-
case scalar_kind_t::f64_k:
|
|
1530
|
-
default: return
|
|
1551
|
+
void reset_cos_metric_() noexcept {
|
|
1552
|
+
switch (scalar_kind_) {
|
|
1553
|
+
case scalar_kind_t::i8_k: return reset_cos_metric_i8_();
|
|
1554
|
+
case scalar_kind_t::f16_k: return reset_cos_metric_f16_();
|
|
1555
|
+
case scalar_kind_t::f32_k: raw_ptr_ = &equidimensional_<metric_cos_gt<f32_t>>; break;
|
|
1556
|
+
case scalar_kind_t::f64_k: raw_ptr_ = &equidimensional_<metric_cos_gt<f64_t>>; break;
|
|
1557
|
+
default: return;
|
|
1531
1558
|
}
|
|
1532
1559
|
}
|
|
1533
1560
|
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
case scalar_kind_t::
|
|
1538
|
-
case scalar_kind_t::
|
|
1539
|
-
|
|
1540
|
-
default: return {};
|
|
1561
|
+
void reset_haversine_metric_() noexcept {
|
|
1562
|
+
switch (scalar_kind_) {
|
|
1563
|
+
case scalar_kind_t::f16_k: raw_ptr_ = &equidimensional_<metric_haversine_gt<f16_t, f32_t>>; break;
|
|
1564
|
+
case scalar_kind_t::f32_k: raw_ptr_ = &equidimensional_<metric_haversine_gt<f32_t>>; break;
|
|
1565
|
+
case scalar_kind_t::f64_k: raw_ptr_ = &equidimensional_<metric_haversine_gt<f64_t>>; break;
|
|
1566
|
+
default: return;
|
|
1541
1567
|
}
|
|
1542
1568
|
}
|
|
1543
1569
|
|
|
1544
|
-
|
|
1545
|
-
switch (
|
|
1546
|
-
case scalar_kind_t::i8_k:
|
|
1547
|
-
case scalar_kind_t::f16_k:
|
|
1548
|
-
case scalar_kind_t::f32_k:
|
|
1549
|
-
case scalar_kind_t::f64_k:
|
|
1550
|
-
default: return
|
|
1570
|
+
void reset_pearson_metric_() noexcept {
|
|
1571
|
+
switch (scalar_kind_) {
|
|
1572
|
+
case scalar_kind_t::i8_k: raw_ptr_ = &equidimensional_<metric_pearson_gt<i8_t, f32_t>>; break;
|
|
1573
|
+
case scalar_kind_t::f16_k: raw_ptr_ = &equidimensional_<metric_pearson_gt<f16_t, f32_t>>; break;
|
|
1574
|
+
case scalar_kind_t::f32_k: raw_ptr_ = &equidimensional_<metric_pearson_gt<f32_t>>; break;
|
|
1575
|
+
case scalar_kind_t::f64_k: raw_ptr_ = &equidimensional_<metric_pearson_gt<f64_t>>; break;
|
|
1576
|
+
default: return;
|
|
1551
1577
|
}
|
|
1552
1578
|
}
|
|
1553
|
-
|
|
1579
|
+
};
|
|
1580
|
+
|
|
1581
|
+
/**
|
|
1582
|
+
* @brief View over a potentially-strided memory buffer, containing a row-major matrix.
|
|
1583
|
+
*/
|
|
1584
|
+
template <typename scalar_at> //
|
|
1585
|
+
class vectors_view_gt {
|
|
1586
|
+
using scalar_t = scalar_at;
|
|
1587
|
+
|
|
1588
|
+
scalar_t const* begin_{};
|
|
1589
|
+
std::size_t dimensions_{};
|
|
1590
|
+
std::size_t count_{};
|
|
1591
|
+
std::size_t stride_bytes_{};
|
|
1592
|
+
|
|
1593
|
+
public:
|
|
1594
|
+
vectors_view_gt() noexcept = default;
|
|
1595
|
+
vectors_view_gt(vectors_view_gt const&) noexcept = default;
|
|
1596
|
+
vectors_view_gt& operator=(vectors_view_gt const&) noexcept = default;
|
|
1597
|
+
|
|
1598
|
+
vectors_view_gt(scalar_t const* begin, std::size_t dimensions, std::size_t count = 1) noexcept
|
|
1599
|
+
: vectors_view_gt(begin, dimensions, count, dimensions * sizeof(scalar_at)) {}
|
|
1600
|
+
|
|
1601
|
+
vectors_view_gt(scalar_t const* begin, std::size_t dimensions, std::size_t count, std::size_t stride_bytes) noexcept
|
|
1602
|
+
: begin_(begin), dimensions_(dimensions), count_(count), stride_bytes_(stride_bytes) {}
|
|
1603
|
+
|
|
1604
|
+
explicit operator bool() const noexcept { return begin_; }
|
|
1605
|
+
std::size_t size() const noexcept { return count_; }
|
|
1606
|
+
std::size_t dimensions() const noexcept { return dimensions_; }
|
|
1607
|
+
std::size_t stride() const noexcept { return stride_bytes_; }
|
|
1608
|
+
scalar_t const* data() const noexcept { return begin_; }
|
|
1609
|
+
scalar_t const* at(std::size_t i) const noexcept {
|
|
1610
|
+
return reinterpret_cast<scalar_t const*>(reinterpret_cast<byte_t const*>(begin_) + i * stride_bytes_);
|
|
1611
|
+
}
|
|
1612
|
+
};
|
|
1613
|
+
|
|
1614
|
+
struct exact_offset_and_distance_t {
|
|
1615
|
+
u32_t offset;
|
|
1616
|
+
f32_t distance;
|
|
1617
|
+
};
|
|
1618
|
+
|
|
1619
|
+
using exact_search_results_t = vectors_view_gt<exact_offset_and_distance_t>;
|
|
1620
|
+
|
|
1621
|
+
/**
|
|
1622
|
+
* @brief Helper-structure for exact search operations.
|
|
1623
|
+
* Perfect if you have @b <1M vectors and @b <100 queries per call.
|
|
1624
|
+
*
|
|
1625
|
+
* Uses a 3-step procedure to minimize:
|
|
1626
|
+
* - cache-misses on vector lookups,
|
|
1627
|
+
* - multi-threaded contention on concurrent writes.
|
|
1628
|
+
*/
|
|
1629
|
+
class exact_search_t {
|
|
1630
|
+
|
|
1631
|
+
inline static bool smaller_distance(exact_offset_and_distance_t a, exact_offset_and_distance_t b) noexcept {
|
|
1632
|
+
return a.distance < b.distance;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
using keys_and_distances_t = buffer_gt<exact_offset_and_distance_t>;
|
|
1636
|
+
keys_and_distances_t keys_and_distances;
|
|
1637
|
+
|
|
1638
|
+
public:
|
|
1639
|
+
template <typename scalar_at, typename executor_at = dummy_executor_t, typename progress_at = dummy_progress_t>
|
|
1640
|
+
exact_search_results_t operator()( //
|
|
1641
|
+
vectors_view_gt<scalar_at> dataset, vectors_view_gt<scalar_at> queries, //
|
|
1642
|
+
std::size_t wanted, metric_punned_t const& metric, //
|
|
1643
|
+
executor_at&& executor = executor_at{}, progress_at&& progress = progress_at{}) {
|
|
1644
|
+
return operator()( //
|
|
1645
|
+
metric, //
|
|
1646
|
+
reinterpret_cast<byte_t const*>(dataset.data()), dataset.size(), dataset.stride(), //
|
|
1647
|
+
reinterpret_cast<byte_t const*>(queries.data()), queries.size(), queries.stride(), //
|
|
1648
|
+
wanted, executor, progress);
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
template <typename executor_at = dummy_executor_t, typename progress_at = dummy_progress_t>
|
|
1652
|
+
exact_search_results_t operator()( //
|
|
1653
|
+
byte_t const* dataset_data, std::size_t dataset_count, std::size_t dataset_stride, //
|
|
1654
|
+
byte_t const* queries_data, std::size_t queries_count, std::size_t queries_stride, //
|
|
1655
|
+
std::size_t wanted, metric_punned_t const& metric, executor_at&& executor = executor_at{},
|
|
1656
|
+
progress_at&& progress = progress_at{}) {
|
|
1657
|
+
|
|
1658
|
+
// Allocate temporary memory to store the distance matrix
|
|
1659
|
+
// Previous version didn't need temporary memory, but the performance was much lower.
|
|
1660
|
+
// In the new design we keep two buffers - original and transposed, as in-place transpositions
|
|
1661
|
+
// of non-rectangular matrixes is expensive.
|
|
1662
|
+
std::size_t tasks_count = dataset_count * queries_count;
|
|
1663
|
+
if (keys_and_distances.size() < tasks_count * 2)
|
|
1664
|
+
keys_and_distances = keys_and_distances_t(tasks_count * 2);
|
|
1665
|
+
if (keys_and_distances.size() < tasks_count * 2)
|
|
1666
|
+
return {};
|
|
1667
|
+
|
|
1668
|
+
exact_offset_and_distance_t* keys_and_distances_per_dataset = keys_and_distances.data();
|
|
1669
|
+
exact_offset_and_distance_t* keys_and_distances_per_query = keys_and_distances_per_dataset + tasks_count;
|
|
1670
|
+
|
|
1671
|
+
// §1. Compute distances in a data-parallel fashion
|
|
1672
|
+
std::atomic<std::size_t> processed{0};
|
|
1673
|
+
executor.dynamic(dataset_count, [&](std::size_t thread_idx, std::size_t dataset_idx) {
|
|
1674
|
+
byte_t const* dataset = dataset_data + dataset_idx * dataset_stride;
|
|
1675
|
+
for (std::size_t query_idx = 0; query_idx != queries_count; ++query_idx) {
|
|
1676
|
+
byte_t const* query = queries_data + query_idx * queries_stride;
|
|
1677
|
+
auto distance = metric(dataset, query);
|
|
1678
|
+
std::size_t task_idx = queries_count * dataset_idx + query_idx;
|
|
1679
|
+
keys_and_distances_per_dataset[task_idx].offset = static_cast<u32_t>(dataset_idx);
|
|
1680
|
+
keys_and_distances_per_dataset[task_idx].distance = static_cast<f32_t>(distance);
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
// It's more efficient in this case to report progress from a single thread
|
|
1684
|
+
processed += queries_count;
|
|
1685
|
+
if (thread_idx == 0)
|
|
1686
|
+
if (!progress(processed.load(), tasks_count))
|
|
1687
|
+
return false;
|
|
1688
|
+
return true;
|
|
1689
|
+
});
|
|
1690
|
+
if (processed.load() != tasks_count)
|
|
1691
|
+
return {};
|
|
1692
|
+
|
|
1693
|
+
// §2. Transpose in a single thread to avoid contention writing into the same memory buffers
|
|
1694
|
+
for (std::size_t query_idx = 0; query_idx != queries_count; ++query_idx) {
|
|
1695
|
+
for (std::size_t dataset_idx = 0; dataset_idx != dataset_count; ++dataset_idx) {
|
|
1696
|
+
std::size_t from_idx = queries_count * dataset_idx + query_idx;
|
|
1697
|
+
std::size_t to_idx = dataset_count * query_idx + dataset_idx;
|
|
1698
|
+
keys_and_distances_per_query[to_idx] = keys_and_distances_per_dataset[from_idx];
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
// §3. Partial-sort every query result
|
|
1703
|
+
executor.fixed(queries_count, [&](std::size_t, std::size_t query_idx) {
|
|
1704
|
+
auto start = keys_and_distances_per_query + dataset_count * query_idx;
|
|
1705
|
+
if (wanted > 1) {
|
|
1706
|
+
// TODO: Consider alternative sorting approaches
|
|
1707
|
+
// radix_sort(start, start + dataset_count, wanted);
|
|
1708
|
+
// std::sort(start, start + dataset_count, &smaller_distance);
|
|
1709
|
+
std::partial_sort(start, start + wanted, start + dataset_count, &smaller_distance);
|
|
1710
|
+
} else {
|
|
1711
|
+
auto min_it = std::min_element(start, start + dataset_count, &smaller_distance);
|
|
1712
|
+
if (min_it != start)
|
|
1713
|
+
std::swap(*min_it, *start);
|
|
1714
|
+
}
|
|
1715
|
+
});
|
|
1716
|
+
|
|
1717
|
+
// At the end report the latest numbers, because the reporter thread may be finished earlier
|
|
1718
|
+
progress(tasks_count, tasks_count);
|
|
1719
|
+
return {keys_and_distances_per_query, wanted, queries_count,
|
|
1720
|
+
dataset_count * sizeof(exact_offset_and_distance_t)};
|
|
1721
|
+
}
|
|
1554
1722
|
};
|
|
1555
1723
|
|
|
1556
1724
|
/**
|
|
@@ -1564,6 +1732,7 @@ class metric_punned_t {
|
|
|
1564
1732
|
* @section Layout
|
|
1565
1733
|
*
|
|
1566
1734
|
* For every slot we store 2 extra bits for 3 possible states: empty, populated, or deleted.
|
|
1735
|
+
* With linear probing the hashes at the end of the populated region will spill into its first half.
|
|
1567
1736
|
*/
|
|
1568
1737
|
template <typename element_at, typename hash_at, typename equals_at, typename allocator_at = std::allocator<char>>
|
|
1569
1738
|
class flat_hash_multi_set_gt {
|
|
@@ -1782,19 +1951,16 @@ class flat_hash_multi_set_gt {
|
|
|
1782
1951
|
using pointer = element_t*;
|
|
1783
1952
|
using reference = element_t&;
|
|
1784
1953
|
|
|
1785
|
-
equal_iterator_gt(std::size_t index, flat_hash_multi_set_gt* parent, const
|
|
1786
|
-
const
|
|
1954
|
+
equal_iterator_gt(std::size_t index, flat_hash_multi_set_gt* parent, query_at const& query,
|
|
1955
|
+
equals_t const& equals)
|
|
1787
1956
|
: index_(index), parent_(parent), query_(query), equals_(equals) {}
|
|
1788
1957
|
|
|
1789
1958
|
// Pre-increment
|
|
1790
1959
|
equal_iterator_gt& operator++() {
|
|
1791
1960
|
do {
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
}
|
|
1796
|
-
} while (!equals_(parent_->slot_ref(index_).element, query_) ||
|
|
1797
|
-
!(parent_->slot_ref(index_).header.populated & parent_->slot_ref(index_).mask));
|
|
1961
|
+
index_ = (index_ + 1) & (parent_->capacity_slots_ - 1);
|
|
1962
|
+
} while (!equals_(parent_->slot_ref(index_).element, query_) &&
|
|
1963
|
+
(parent_->slot_ref(index_).header.populated & parent_->slot_ref(index_).mask));
|
|
1798
1964
|
return *this;
|
|
1799
1965
|
}
|
|
1800
1966
|
|
|
@@ -1818,6 +1984,12 @@ class flat_hash_multi_set_gt {
|
|
|
1818
1984
|
equals_t equals_; // Store the equals functor
|
|
1819
1985
|
};
|
|
1820
1986
|
|
|
1987
|
+
/**
|
|
1988
|
+
* @brief Returns an iterator range of all elements matching the given query.
|
|
1989
|
+
*
|
|
1990
|
+
* Technically, the second iterator points to the first empty slot after a
|
|
1991
|
+
* range of equal values and non-equal values with similar hashes.
|
|
1992
|
+
*/
|
|
1821
1993
|
template <typename query_at>
|
|
1822
1994
|
std::pair<equal_iterator_gt<query_at>, equal_iterator_gt<query_at>>
|
|
1823
1995
|
equal_range(query_at const& query) const noexcept {
|
|
@@ -1830,28 +2002,41 @@ class flat_hash_multi_set_gt {
|
|
|
1830
2002
|
|
|
1831
2003
|
hash_t hasher;
|
|
1832
2004
|
std::size_t hash_value = hasher(query);
|
|
1833
|
-
std::size_t
|
|
1834
|
-
std::size_t const start_index =
|
|
1835
|
-
std::size_t first_equal_index = capacity_slots_;
|
|
2005
|
+
std::size_t first_equal_index = hash_value & (capacity_slots_ - 1);
|
|
2006
|
+
std::size_t const start_index = first_equal_index;
|
|
1836
2007
|
|
|
1837
2008
|
// Linear probing to find the first equal element
|
|
1838
2009
|
do {
|
|
1839
|
-
slot_ref_t slot = slot_ref(
|
|
2010
|
+
slot_ref_t slot = slot_ref(first_equal_index);
|
|
1840
2011
|
if (slot.header.populated & ~slot.header.deleted & slot.mask) {
|
|
1841
|
-
if (equals(slot.element, query))
|
|
1842
|
-
first_equal_index = slot_index;
|
|
2012
|
+
if (equals(slot.element, query))
|
|
1843
2013
|
break;
|
|
1844
|
-
}
|
|
1845
2014
|
}
|
|
1846
2015
|
// Stop if we find an empty slot
|
|
1847
2016
|
else if (~slot.header.populated & slot.mask)
|
|
1848
|
-
|
|
2017
|
+
return {end, end};
|
|
1849
2018
|
|
|
1850
2019
|
// Move to the next slot
|
|
1851
|
-
|
|
1852
|
-
} while (
|
|
2020
|
+
first_equal_index = (first_equal_index + 1) & (capacity_slots_ - 1);
|
|
2021
|
+
} while (first_equal_index != start_index);
|
|
2022
|
+
|
|
2023
|
+
// If no matching element was found, return end iterators
|
|
2024
|
+
if (first_equal_index == capacity_slots_)
|
|
2025
|
+
return {end, end};
|
|
2026
|
+
|
|
2027
|
+
// Start from the first matching element and find the end of the populated range
|
|
2028
|
+
std::size_t first_empty_index = first_equal_index;
|
|
2029
|
+
do {
|
|
2030
|
+
first_empty_index = (first_empty_index + 1) & (capacity_slots_ - 1);
|
|
2031
|
+
slot_ref_t slot = slot_ref(first_empty_index);
|
|
1853
2032
|
|
|
1854
|
-
|
|
2033
|
+
// If we find an empty slot, this is our end
|
|
2034
|
+
if (~slot.header.populated & slot.mask)
|
|
2035
|
+
break;
|
|
2036
|
+
} while (first_empty_index != start_index);
|
|
2037
|
+
|
|
2038
|
+
return {equal_iterator_gt<query_at>(first_equal_index, this_ptr, query, equals),
|
|
2039
|
+
equal_iterator_gt<query_at>(first_empty_index, this_ptr, query, equals)};
|
|
1855
2040
|
}
|
|
1856
2041
|
|
|
1857
2042
|
template <typename similar_at> bool pop_first(similar_at&& query, element_t& popped_value) noexcept {
|
|
@@ -1897,7 +2082,7 @@ class flat_hash_multi_set_gt {
|
|
|
1897
2082
|
equals_t equals;
|
|
1898
2083
|
std::size_t hash_value = hasher(query);
|
|
1899
2084
|
std::size_t slot_index = hash_value & (capacity_slots_ - 1); // Assuming capacity_slots_ is a power of 2
|
|
1900
|
-
std::size_t start_index = slot_index;
|
|
2085
|
+
std::size_t const start_index = slot_index; // To detect loop in probing
|
|
1901
2086
|
std::size_t count = 0; // Count of elements removed
|
|
1902
2087
|
|
|
1903
2088
|
// Linear probing to find all matches
|