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.
Files changed (54) hide show
  1. package/. npmignore +14 -0
  2. package/.cmake-format.py +19 -0
  3. package/.github/workflows/Dockerfile.libs +3 -3
  4. package/.github/workflows/prerelease.yml +85 -64
  5. package/.github/workflows/release.yml +18 -16
  6. package/.gitignore +1 -1
  7. package/.vscode/launch.json +47 -14
  8. package/.vscode/settings.json +7 -0
  9. package/.vscode/tasks.json +44 -8
  10. package/CITATION.cff +1 -1
  11. package/CMakeLists.txt +218 -67
  12. package/Cargo.toml +1 -1
  13. package/README.md +1 -1
  14. package/binding.gyp +3 -2
  15. package/build.gradle +19 -10
  16. package/c/CMakeLists.txt +21 -48
  17. package/c/README.md +5 -5
  18. package/c/lib.cpp +52 -7
  19. package/c/test.c +137 -94
  20. package/c/usearch.h +42 -0
  21. package/cmake/clipp.cmake +0 -1
  22. package/cmake/jemalloc.cmake +21 -37
  23. package/conanfile.py +1 -1
  24. package/cpp/CMakeLists.txt +14 -123
  25. package/cpp/bench.cpp +0 -12
  26. package/cpp/test.cpp +29 -1
  27. package/csharp/nuget/nuget-package.props +1 -1
  28. package/docs/compilation.md +23 -16
  29. package/docs/conf.py +1 -1
  30. package/golang/README.md +52 -51
  31. package/golang/lib.go +1 -1
  32. package/golang/lib_test.go +4 -1
  33. package/include/usearch/index.hpp +2 -2
  34. package/include/usearch/index_plugins.hpp +345 -160
  35. package/javascript/lib.cpp +204 -250
  36. package/javascript/usearch.js +440 -2
  37. package/javascript/usearch.test.js +37 -0
  38. package/package.json +10 -5
  39. package/python/lib.cpp +96 -225
  40. package/python/scripts/bench_exact.py +2 -2
  41. package/python/scripts/test_jit.py +2 -13
  42. package/python/scripts/test_sparse.py +1 -1
  43. package/python/scripts/test_tooling.py +26 -5
  44. package/rust/lib.cpp +12 -4
  45. package/rust/lib.hpp +7 -0
  46. package/rust/lib.rs +170 -10
  47. package/setup.py +14 -4
  48. package/wasm/CMakeLists.txt +4 -12
  49. package/wasm/lib.cpp +1 -1
  50. package/wasmer.toml +1 -1
  51. package/javascript/docs.js +0 -111
  52. package/javascript/test.js +0 -32
  53. package/javascript/usearch.d.ts +0 -110
  54. 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
- #else
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 Type-punned metric class, that can wrap an existing STL `std::function`.
1334
- * Additional annotation is useful for
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
- stl_function_t stl_function_;
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
- * ! The only relevant function in the object. Everything else is just dynamic dispatch.
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 { return stl_function_(a, b); }
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
- stl_function_t stl_function, std::size_t dimensions = 0, metric_kind_t metric_kind = metric_kind_t::unknown_k,
1362
- scalar_kind_t scalar_kind = scalar_kind_t::unknown_k, isa_kind_t isa_kind = isa_kind_t::auto_k)
1363
- : stl_function_(stl_function), dimensions_(dimensions), metric_kind_(metric_kind), scalar_kind_(scalar_kind),
1364
- isa_kind_(isa_kind) {}
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
- inline metric_punned_t( //
1367
- std::size_t dimensions, metric_kind_t metric_kind = metric_kind_t::cos_k,
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
- static metric_punned_t make_(std::size_t bytes_per_vector, metric_kind_t metric_kind, scalar_kind_t scalar_kind) {
1389
-
1390
- switch (metric_kind) {
1391
- case metric_kind_t::ip_k: return ip_metric_(bytes_per_vector, scalar_kind);
1392
- case metric_kind_t::cos_k: return cos_metric_(bytes_per_vector, scalar_kind);
1393
- case metric_kind_t::l2sq_k: return l2sq_metric_(bytes_per_vector, scalar_kind);
1394
- case metric_kind_t::pearson_k: return pearson_metric_(bytes_per_vector, scalar_kind);
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
- return {to_stl_<metric_tanimoto_gt<b1x8_t>>(bytes_per_vector), bytes_per_vector, metric_kind_t::tanimoto_k,
1404
- scalar_kind_t::b1x8_k, isa_kind_t::auto_k};
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
- return {to_stl_<metric_sorensen_gt<b1x8_t>>(bytes_per_vector), bytes_per_vector, metric_kind_t::sorensen_k,
1408
- scalar_kind_t::b1x8_k, isa_kind_t::auto_k};
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> static stl_function_t to_stl_(std::size_t bytes) {
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
- return [=](byte_t const* a, byte_t const* b) -> result_t {
1417
- return typed_at{}((scalar_t const*)a, (scalar_t const*)b, bytes / sizeof(scalar_t));
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
- static stl_function_t pun_stl_(std::function<result_t(scalar_at const*, scalar_at const*)> typed) {
1423
- return [=](byte_t const* a, byte_t const* b) -> result_t {
1424
- return typed((scalar_at const*)a, (scalar_at const*)b);
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
- static metric_punned_t ip_metric_f32_(std::size_t bytes_per_vector) {
1465
+ void reset_ip_metric_f32_() noexcept {
1430
1466
  #if USEARCH_USE_SIMSIMD
1431
1467
  #if SIMSIMD_TARGET_ARM_NEON
1432
- if (hardware_supports(isa_kind_t::neon_k) && bytes_per_vector % 16 == 0) return {pun_stl_<f32_t>([=](f32_t const* a, f32_t const* b) { return simsimd_dot_f32x4_neon(a, b, bytes_per_vector / 4); }), bytes_per_vector, metric_kind_t::ip_k, scalar_kind_t::f32_k, isa_kind_t::neon_k};
1433
- #endif
1434
- #if SIMSIMD_TARGET_ARM_SVE
1435
- if (hardware_supports(isa_kind_t::sve_k)) return {pun_stl_<f32_t>([=](f32_t const* a, f32_t const* b) { return simsimd_dot_f32_sve(a, b, bytes_per_vector / 4); }), bytes_per_vector, metric_kind_t::ip_k, scalar_kind_t::f32_k, isa_kind_t::sve_k};
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
- #endif
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
- static metric_punned_t cos_metric_f16_(std::size_t bytes_per_vector) {
1478
+ void reset_cos_metric_f16_() noexcept {
1445
1479
  #if USEARCH_USE_SIMSIMD
1446
1480
  #if SIMSIMD_TARGET_X86_AVX512
1447
- if (hardware_supports(isa_kind_t::avx512f16_k) && bytes_per_vector % 32 == 0) return {pun_stl_<simsimd_f16_t>([=](simsimd_f16_t const* a, simsimd_f16_t const* b) { return simsimd_cos_f16x16_avx512(a, b, bytes_per_vector / 2); }), bytes_per_vector, metric_kind_t::cos_k, scalar_kind_t::f16_k, isa_kind_t::avx512f16_k};
1448
- #endif
1449
- #if SIMSIMD_TARGET_X86_AVX2
1450
- 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_cos_f16x8_avx2(a, b, bytes_per_vector / 2); }), bytes_per_vector, metric_kind_t::cos_k, scalar_kind_t::f16_k, isa_kind_t::avx2f16_k};
1451
- #endif
1452
- #if SIMSIMD_TARGET_ARM_NEON
1453
- 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_cos_f16x4_neon(a, b, bytes_per_vector / 2); }), bytes_per_vector, metric_kind_t::cos_k, scalar_kind_t::f16_k, isa_kind_t::neon_k};
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
- return {to_stl_<metric_cos_gt<f16_t, f32_t>>(bytes_per_vector), bytes_per_vector, metric_kind_t::cos_k, scalar_kind_t::f16_k, isa_kind_t::auto_k};
1490
+ raw_ptr_ = &equidimensional_<metric_cos_gt<f16_t, f32_t>>;
1460
1491
  }
1461
1492
 
1462
- static metric_punned_t ip_metric_f16_(std::size_t bytes_per_vector) {
1493
+ void reset_ip_metric_f16_() noexcept {
1463
1494
  #if USEARCH_USE_SIMSIMD
1464
1495
  #if SIMSIMD_TARGET_X86_AVX512
1465
- if (hardware_supports(isa_kind_t::avx512f16_k) && bytes_per_vector % 32 == 0) return {pun_stl_<simsimd_f16_t>([=](simsimd_f16_t const* a, simsimd_f16_t const* b) { return simsimd_dot_f16x16_avx512(a, b, bytes_per_vector / 2); }), bytes_per_vector, metric_kind_t::ip_k, scalar_kind_t::f16_k, isa_kind_t::avx512f16_k};
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
- #if SIMSIMD_TARGET_ARM_NEON
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
- static metric_punned_t l2sq_metric_f16_(std::size_t bytes_per_vector) {
1506
+ void reset_l2sq_metric_f16_() noexcept {
1478
1507
  #if USEARCH_USE_SIMSIMD
1479
1508
  #if SIMSIMD_TARGET_X86_AVX512
1480
- if (hardware_supports(isa_kind_t::avx512f16_k) && bytes_per_vector % 32 == 0) return {pun_stl_<simsimd_f16_t>([=](simsimd_f16_t const* a, simsimd_f16_t const* b) { return simsimd_l2sq_f16x16_avx512(a, b, bytes_per_vector / 2); }), bytes_per_vector, metric_kind_t::cos_k, scalar_kind_t::f16_k, isa_kind_t::avx512f16_k};
1481
- #endif
1482
- #if SIMSIMD_TARGET_X86_AVX2
1483
- 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_l2sq_f16x8_avx2(a, b, bytes_per_vector / 2); }), bytes_per_vector, metric_kind_t::cos_k, scalar_kind_t::f16_k, isa_kind_t::avx2f16_k};
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
- #endif
1489
- return {to_stl_<metric_l2sq_gt<f16_t, f32_t>>(bytes_per_vector), bytes_per_vector, metric_kind_t::l2sq_k, scalar_kind_t::f16_k, isa_kind_t::auto_k};
1490
- }
1516
+ raw_ptr_ = &equidimensional_<metric_l2sq_gt<f16_t, f32_t>>;
1517
+ }
1491
1518
 
1492
- static metric_punned_t cos_metric_i8_(std::size_t bytes_per_vector) {
1519
+ void reset_cos_metric_i8_() noexcept {
1493
1520
  #if USEARCH_USE_SIMSIMD
1494
1521
  #if SIMSIMD_TARGET_ARM_NEON
1495
- if (hardware_supports(isa_kind_t::neon_k) && bytes_per_vector % 16 == 0) return {pun_stl_<int8_t>([=](int8_t const* a, int8_t const* b) { return simsimd_cos_i8x16_neon(a, b, bytes_per_vector); }), bytes_per_vector, metric_kind_t::cos_k, scalar_kind_t::i8_k, isa_kind_t::neon_k};
1496
- #endif
1497
- #if SIMSIMD_TARGET_X86_AVX2
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
- return {to_stl_<cos_i8_t>(bytes_per_vector), bytes_per_vector, metric_kind_t::cos_k, scalar_kind_t::i8_k, isa_kind_t::auto_k};
1527
+ raw_ptr_ = &equidimensional_<cos_i8_t>;
1502
1528
  }
1529
+ // clang-format on
1503
1530
 
1504
- static metric_punned_t ip_metric_(std::size_t bytes_per_vector, scalar_kind_t scalar_kind) {
1505
- switch (scalar_kind) { // The two most common numeric types for the most common metric have optimized versions
1506
- case scalar_kind_t::f32_k: return ip_metric_f32_(bytes_per_vector);
1507
- case scalar_kind_t::f16_k: return ip_metric_f16_(bytes_per_vector);
1508
- case scalar_kind_t::i8_k: return cos_metric_i8_(bytes_per_vector);
1509
- case scalar_kind_t::f64_k: return {to_stl_<metric_ip_gt<f64_t>>(bytes_per_vector), bytes_per_vector, metric_kind_t::ip_k, scalar_kind_t::f64_k, isa_kind_t::auto_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
- static metric_punned_t l2sq_metric_(std::size_t bytes_per_vector, scalar_kind_t scalar_kind) {
1515
- switch (scalar_kind) {
1516
- case scalar_kind_t::i8_k: return {to_stl_<l2sq_i8_t>(bytes_per_vector), bytes_per_vector, metric_kind_t::l2sq_k, scalar_kind_t::i8_k, isa_kind_t::auto_k};
1517
- case scalar_kind_t::f16_k: return l2sq_metric_f16_(bytes_per_vector);
1518
- case scalar_kind_t::f32_k: return {to_stl_<metric_l2sq_gt<f32_t>>(bytes_per_vector), bytes_per_vector, metric_kind_t::l2sq_k, scalar_kind_t::f32_k, isa_kind_t::auto_k};
1519
- case scalar_kind_t::f64_k: return {to_stl_<metric_l2sq_gt<f64_t>>(bytes_per_vector), bytes_per_vector, metric_kind_t::l2sq_k, scalar_kind_t::f64_k, isa_kind_t::auto_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
- static metric_punned_t cos_metric_(std::size_t bytes_per_vector, scalar_kind_t scalar_kind) {
1525
- switch (scalar_kind) {
1526
- case scalar_kind_t::i8_k: return cos_metric_i8_(bytes_per_vector);
1527
- case scalar_kind_t::f16_k: return cos_metric_f16_(bytes_per_vector);
1528
- case scalar_kind_t::f32_k: return {to_stl_<metric_cos_gt<f32_t>>(bytes_per_vector), bytes_per_vector, metric_kind_t::cos_k, scalar_kind_t::f32_k, isa_kind_t::auto_k};
1529
- case scalar_kind_t::f64_k: return {to_stl_<metric_cos_gt<f64_t>>(bytes_per_vector), bytes_per_vector, metric_kind_t::cos_k, scalar_kind_t::f64_k, isa_kind_t::auto_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
- static metric_punned_t haversine_metric_(scalar_kind_t scalar_kind) {
1535
- std::size_t bytes_per_vector = 2u * bits_per_scalar(scalar_kind) / CHAR_BIT;
1536
- switch (scalar_kind) {
1537
- case scalar_kind_t::f16_k: return {to_stl_<metric_haversine_gt<f16_t, f32_t>>(bytes_per_vector), bytes_per_vector, metric_kind_t::haversine_k, scalar_kind_t::f16_k, isa_kind_t::auto_k};
1538
- case scalar_kind_t::f32_k: return {to_stl_<metric_haversine_gt<f32_t>>(bytes_per_vector), bytes_per_vector, metric_kind_t::haversine_k, scalar_kind_t::f32_k, isa_kind_t::auto_k};
1539
- case scalar_kind_t::f64_k: return {to_stl_<metric_haversine_gt<f64_t>>(bytes_per_vector), bytes_per_vector, metric_kind_t::haversine_k, scalar_kind_t::f64_k, isa_kind_t::auto_k};
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
- static metric_punned_t pearson_metric_(std::size_t bytes_per_vector, scalar_kind_t scalar_kind) {
1545
- switch (scalar_kind) {
1546
- case scalar_kind_t::i8_k: return {to_stl_<metric_pearson_gt<i8_t, f32_t>>(bytes_per_vector), bytes_per_vector, metric_kind_t::pearson_k, scalar_kind_t::i8_k, isa_kind_t::auto_k};
1547
- case scalar_kind_t::f16_k: return {to_stl_<metric_pearson_gt<f16_t, f32_t>>(bytes_per_vector), bytes_per_vector, metric_kind_t::pearson_k, scalar_kind_t::f16_k, isa_kind_t::auto_k};
1548
- case scalar_kind_t::f32_k: return {to_stl_<metric_pearson_gt<f32_t>>(bytes_per_vector), bytes_per_vector, metric_kind_t::pearson_k, scalar_kind_t::f32_k, isa_kind_t::auto_k};
1549
- case scalar_kind_t::f64_k: return {to_stl_<metric_pearson_gt<f64_t>>(bytes_per_vector), bytes_per_vector, metric_kind_t::pearson_k, scalar_kind_t::f64_k, isa_kind_t::auto_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
- // clang-format on
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 query_at& query,
1786
- const equals_t& equals)
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
- ++index_;
1793
- if (index_ >= parent_->capacity_slots_) {
1794
- break;
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 slot_index = hash_value & (capacity_slots_ - 1);
1834
- std::size_t const start_index = slot_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(slot_index);
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
- break;
2017
+ return {end, end};
1849
2018
 
1850
2019
  // Move to the next slot
1851
- slot_index = (slot_index + 1) & (capacity_slots_ - 1);
1852
- } while (slot_index != start_index);
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
- return {equal_iterator_gt<query_at>(first_equal_index, this_ptr, query, equals), end};
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; // To detect loop in probing
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