usearch 0.20.0 → 0.21.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.
@@ -258,10 +258,13 @@ template <typename scalar_at> class span_gt {
258
258
  std::size_t size_;
259
259
 
260
260
  public:
261
- span_gt(scalar_at* begin, scalar_at const* end) noexcept : data_(begin), size_(end - begin) {}
261
+ span_gt() noexcept : data_(nullptr), size_(0u) {}
262
+ span_gt(scalar_at* begin, scalar_at* end) noexcept : data_(begin), size_(end - begin) {}
262
263
  span_gt(scalar_at* begin, std::size_t size) noexcept : data_(begin), size_(size) {}
263
264
  scalar_at* data() const noexcept { return data_; }
264
265
  std::size_t size() const noexcept { return size_; }
266
+ scalar_at* begin() const noexcept { return data_; }
267
+ scalar_at* end() const noexcept { return data_ + size_; }
265
268
  operator scalar_at*() const noexcept { return data(); }
266
269
  };
267
270
 
@@ -617,8 +620,8 @@ template <typename result_at> struct expected_gt {
617
620
  }
618
621
  result_at const& operator*() const noexcept { return result; }
619
622
  explicit operator bool() const noexcept { return !error; }
620
- expected_gt failed(char const* message) noexcept {
621
- error = message;
623
+ expected_gt failed(error_t message) noexcept {
624
+ error = std::move(message);
622
625
  return std::move(*this);
623
626
  }
624
627
  };
@@ -1007,7 +1010,7 @@ class usearch_pack_m uint40_t {
1007
1010
 
1008
1011
  inline uint40_t& operator+=(std::size_t n) noexcept {
1009
1012
  unsigned char* n_octets = reinterpret_cast<unsigned char*>(&n);
1010
- std::uint32_t& n_tail = *reinterpret_cast<std::uint32_t*>(&n);
1013
+ std::uint32_t& n_tail = *reinterpret_cast<std::uint32_t*>(n_octets);
1011
1014
  std::uint32_t& tail = *reinterpret_cast<std::uint32_t*>(octets);
1012
1015
  octets[4] += static_cast<unsigned char>((tail + n_tail) < tail);
1013
1016
  tail += n_tail;
@@ -1042,6 +1045,89 @@ class usearch_pack_m uint40_t {
1042
1045
 
1043
1046
  static_assert(sizeof(uint40_t) == 5, "uint40_t must be exactly 5 bytes");
1044
1047
 
1048
+ template <typename element_at, typename allocator_at = std::allocator<element_at>> //
1049
+ class ring_gt {
1050
+ public:
1051
+ using element_t = element_at;
1052
+ using allocator_t = allocator_at;
1053
+
1054
+ static_assert(std::is_trivially_destructible<element_t>(), "This heap is designed for trivial structs");
1055
+ static_assert(std::is_trivially_copy_constructible<element_t>(), "This heap is designed for trivial structs");
1056
+
1057
+ using value_type = element_t;
1058
+
1059
+ private:
1060
+ element_t* elements_;
1061
+ std::size_t capacity_;
1062
+ std::size_t head_;
1063
+ std::size_t tail_;
1064
+ bool empty_;
1065
+ allocator_t allocator_;
1066
+
1067
+ public:
1068
+ explicit ring_gt(allocator_t const& alloc = allocator_t()) noexcept
1069
+ : elements_(nullptr), capacity_(0), head_(0), tail_(0), empty_(true), allocator_(alloc) {}
1070
+
1071
+ ring_gt(ring_gt const&) = delete;
1072
+ ring_gt& operator=(ring_gt const&) = delete;
1073
+
1074
+ ~ring_gt() noexcept { reset(); }
1075
+
1076
+ bool empty() const noexcept { return empty_; }
1077
+ size_t size() const noexcept {
1078
+ if (empty_)
1079
+ return 0;
1080
+ else if (head_ >= tail_)
1081
+ return head_ - tail_;
1082
+ else
1083
+ return capacity_ - (tail_ - head_);
1084
+ }
1085
+
1086
+ void reset() noexcept {
1087
+ if (elements_)
1088
+ allocator_.deallocate(elements_, capacity_);
1089
+ elements_ = nullptr;
1090
+ capacity_ = 0;
1091
+ head_ = 0;
1092
+ tail_ = 0;
1093
+ empty_ = true;
1094
+ }
1095
+
1096
+ bool resize(std::size_t n) noexcept {
1097
+ element_t* elements = allocator_.allocate(n);
1098
+ if (!elements)
1099
+ return false;
1100
+ reset();
1101
+ elements_ = elements;
1102
+ capacity_ = n;
1103
+ return true;
1104
+ }
1105
+
1106
+ void push(element_t const& value) noexcept {
1107
+ elements_[head_] = value;
1108
+ head_ = (head_ + 1) % capacity_;
1109
+ empty_ = false;
1110
+ }
1111
+
1112
+ bool try_push(element_t const& value) noexcept {
1113
+ if (head_ == tail_ && !empty_)
1114
+ return false; // elements_ is full
1115
+
1116
+ return push(value);
1117
+ return true;
1118
+ }
1119
+
1120
+ bool try_pop(element_t& value) noexcept {
1121
+ if (empty_)
1122
+ return false;
1123
+
1124
+ value = std::move(elements_[tail_]);
1125
+ tail_ = (tail_ + 1) % capacity_;
1126
+ empty_ = head_ == tail_;
1127
+ return true;
1128
+ }
1129
+ };
1130
+
1045
1131
  /// @brief Number of neighbors per graph node.
1046
1132
  /// Defaults to 32 in FAISS and 16 in hnswlib.
1047
1133
  /// > It is called `M` in the paper.
@@ -1123,6 +1209,21 @@ struct search_config_t {
1123
1209
  bool exact = false;
1124
1210
  };
1125
1211
 
1212
+ struct copy_config_t {
1213
+ bool copy_vectors = true;
1214
+ };
1215
+
1216
+ struct join_config_t {
1217
+ /// @brief Controls maximum number of proposals per man during stable marriage.
1218
+ std::size_t max_proposals = 0;
1219
+ /// @brief Hyper-parameter controlling the quality of search.
1220
+ /// Defaults to 16 in FAISS and 10 in hnswlib.
1221
+ /// > It is called `ef` in the paper.
1222
+ std::size_t expansion = default_expansion_search();
1223
+ /// @brief Brute-forces exhaustive search over all entries in the index.
1224
+ bool exact = false;
1225
+ };
1226
+
1126
1227
  using file_header_t = byte_t[64];
1127
1228
 
1128
1229
  /**
@@ -1227,8 +1328,8 @@ struct file_head_result_t {
1227
1328
  error_t error;
1228
1329
 
1229
1330
  explicit operator bool() const noexcept { return !error; }
1230
- file_head_result_t failed(char const* message) noexcept {
1231
- error = message;
1331
+ file_head_result_t failed(error_t message) noexcept {
1332
+ error = std::move(message);
1232
1333
  return std::move(*this);
1233
1334
  }
1234
1335
  };
@@ -1262,6 +1363,37 @@ struct viewed_file_t {
1262
1363
  };
1263
1364
  #endif
1264
1365
 
1366
+ struct dummy_predicate_t {
1367
+ template <typename match_at> constexpr bool operator()(match_at&&) const noexcept { return true; }
1368
+ };
1369
+
1370
+ struct dummy_progress_t {
1371
+ inline void operator()(std::size_t /*progress*/, std::size_t /*total*/) const noexcept {}
1372
+ };
1373
+
1374
+ struct dummy_executor_t {
1375
+ dummy_executor_t() noexcept {}
1376
+ std::size_t size() const noexcept { return 1; }
1377
+
1378
+ template <typename thread_aware_function_at>
1379
+ void execute_bulk(std::size_t tasks, thread_aware_function_at&& thread_aware_function) noexcept {
1380
+ for (std::size_t task_idx = 0; task_idx != tasks; ++task_idx)
1381
+ thread_aware_function(0, task_idx);
1382
+ }
1383
+
1384
+ template <typename thread_aware_function_at>
1385
+ void execute_bulk(thread_aware_function_at&& thread_aware_function) noexcept {
1386
+ thread_aware_function(0);
1387
+ }
1388
+ };
1389
+
1390
+ struct dummy_label_to_label_mapping_t {
1391
+ struct member_ref_t {
1392
+ template <typename label_at> member_ref_t& operator=(label_at&&) noexcept { return *this; }
1393
+ };
1394
+ template <typename label_at> member_ref_t operator[](label_at&&) const noexcept { return {}; }
1395
+ };
1396
+
1265
1397
  /**
1266
1398
  * @brief Approximate Nearest Neighbors Search index using the
1267
1399
  * Hierarchical Navigable Small World graph algorithm.
@@ -1552,7 +1684,7 @@ class index_gt {
1552
1684
  index_config_t config_{};
1553
1685
  index_limits_t limits_{};
1554
1686
  metric_t metric_{};
1555
- allocator_t allocator_{};
1687
+ mutable allocator_t allocator_{};
1556
1688
  point_allocator_t point_allocator_{};
1557
1689
  precomputed_constants_t pre_{};
1558
1690
  viewed_file_t viewed_file_{};
@@ -1606,6 +1738,35 @@ class index_gt {
1606
1738
  return *this;
1607
1739
  }
1608
1740
 
1741
+ struct copy_result_t {
1742
+ error_t error;
1743
+ index_gt index;
1744
+
1745
+ explicit operator bool() const noexcept { return !error; }
1746
+ copy_result_t failed(error_t message) noexcept {
1747
+ error = std::move(message);
1748
+ return std::move(*this);
1749
+ }
1750
+ };
1751
+
1752
+ copy_result_t copy(copy_config_t /*config*/ = {}) const noexcept {
1753
+ copy_result_t result;
1754
+ index_gt& other = result.index;
1755
+ other = index_gt(config_, metric_, allocator_, point_allocator_);
1756
+ if (!other.reserve(limits_))
1757
+ return result.failed("Failed to reserve the contexts");
1758
+
1759
+ // Now all is left - is to allocate new `node_t` instances and populate
1760
+ // the `other.nodes_` array into it.
1761
+ for (std::size_t i = 0; i != size_; ++i)
1762
+ other.nodes_[i] = other.node_make_copy_(node_bytes_split_(nodes_[i]));
1763
+
1764
+ other.size_ = size_.load();
1765
+ other.max_level_ = max_level_;
1766
+ other.entry_id_ = entry_id_;
1767
+ return result;
1768
+ }
1769
+
1609
1770
  member_citerator_t cbegin() const noexcept { return {this, 0}; }
1610
1771
  member_citerator_t cend() const noexcept { return {this, size()}; }
1611
1772
  member_citerator_t begin() const noexcept { return {this, 0}; }
@@ -1657,11 +1818,13 @@ class index_gt {
1657
1818
  std::swap(limits_, other.limits_);
1658
1819
  std::swap(metric_, other.metric_);
1659
1820
  std::swap(allocator_, other.allocator_);
1821
+ std::swap(point_allocator_, other.point_allocator_);
1660
1822
  std::swap(pre_, other.pre_);
1661
1823
  std::swap(viewed_file_, other.viewed_file_);
1662
1824
  std::swap(max_level_, other.max_level_);
1663
1825
  std::swap(entry_id_, other.entry_id_);
1664
1826
  std::swap(nodes_, other.nodes_);
1827
+ std::swap(nodes_mutexes_, other.nodes_mutexes_);
1665
1828
  std::swap(contexts_, other.contexts_);
1666
1829
 
1667
1830
  // Non-atomic parts.
@@ -1760,8 +1923,8 @@ class index_gt {
1760
1923
  id_t id{};
1761
1924
 
1762
1925
  explicit operator bool() const noexcept { return !error; }
1763
- add_result_t failed(char const* message) noexcept {
1764
- error = message;
1926
+ add_result_t failed(error_t message) noexcept {
1927
+ error = std::move(message);
1765
1928
  return std::move(*this);
1766
1929
  }
1767
1930
  };
@@ -1793,14 +1956,17 @@ class index_gt {
1793
1956
  inline search_result_t& operator=(search_result_t&&) = default;
1794
1957
 
1795
1958
  explicit operator bool() const noexcept { return !error; }
1796
- search_result_t failed(char const* message) noexcept {
1797
- error = message;
1959
+ search_result_t failed(error_t message) noexcept {
1960
+ error = std::move(message);
1798
1961
  return std::move(*this);
1799
1962
  }
1800
1963
 
1801
1964
  inline operator std::size_t() const noexcept { return count; }
1802
1965
  inline std::size_t size() const noexcept { return count; }
1966
+ inline bool empty() const noexcept { return !count; }
1803
1967
  inline match_t operator[](std::size_t i) const noexcept { return at(i); }
1968
+ inline match_t front() const noexcept { return at(0); }
1969
+ inline match_t back() const noexcept { return at(count - 1); }
1804
1970
  inline bool contains(label_t label) const noexcept {
1805
1971
  for (std::size_t i = 0; i != count; ++i)
1806
1972
  if (at(i).member.label == label)
@@ -1866,7 +2032,7 @@ class index_gt {
1866
2032
  new_level_lock.unlock();
1867
2033
 
1868
2034
  // Allocate the neighbors
1869
- node_t node = node_malloc_(label, vector, target_level, config.store_vector);
2035
+ node_t node = node_make_(label, vector, target_level, config.store_vector);
1870
2036
  if (!node)
1871
2037
  return result.failed("Out of memory!");
1872
2038
  std::size_t old_size = size_.fetch_add(1);
@@ -1910,10 +2076,6 @@ class index_gt {
1910
2076
  return result;
1911
2077
  }
1912
2078
 
1913
- struct dummy_predicate_t {
1914
- constexpr bool operator()(match_t const&) const noexcept { return true; }
1915
- };
1916
-
1917
2079
  /**
1918
2080
  * @brief Searches for the closest elements to the given ::query. Thread-safe.
1919
2081
  *
@@ -2083,8 +2245,8 @@ class index_gt {
2083
2245
  error_t error;
2084
2246
 
2085
2247
  explicit operator bool() const noexcept { return !error; }
2086
- serialization_result_t failed(char const* message) noexcept {
2087
- error = message;
2248
+ serialization_result_t failed(error_t message) noexcept {
2249
+ error = std::move(message);
2088
2250
  return std::move(*this);
2089
2251
  }
2090
2252
  };
@@ -2094,7 +2256,8 @@ class index_gt {
2094
2256
  * co-locating vectors and neighbors lists.
2095
2257
  * Available on Linux, MacOS, Windows.
2096
2258
  */
2097
- serialization_result_t save(char const* file_path) const noexcept {
2259
+ template <typename progress_at = dummy_progress_t>
2260
+ serialization_result_t save(char const* file_path, progress_at&& progress = {}) const noexcept {
2098
2261
 
2099
2262
  // Make sure we have right to write to that file
2100
2263
  serialization_result_t result;
@@ -2164,6 +2327,7 @@ class index_gt {
2164
2327
  write_chunk(node.vector(), node_vector_bytes);
2165
2328
  if (result.error)
2166
2329
  return result;
2330
+ progress(i, state.size);
2167
2331
  }
2168
2332
 
2169
2333
  std::fclose(file);
@@ -2175,7 +2339,8 @@ class index_gt {
2175
2339
  * copying both vectors and neighbors lists into RAM.
2176
2340
  * Available on Linux, MacOS, Windows.
2177
2341
  */
2178
- serialization_result_t load(char const* file_path) noexcept {
2342
+ template <typename progress_at = dummy_progress_t>
2343
+ serialization_result_t load(char const* file_path, progress_at&& progress = {}) noexcept {
2179
2344
 
2180
2345
  serialization_result_t result;
2181
2346
  file_header_t state_buffer{};
@@ -2223,7 +2388,8 @@ class index_gt {
2223
2388
  }
2224
2389
 
2225
2390
  // Load nodes one by one
2226
- for (std::size_t i = 0; i != size_; ++i) {
2391
+ std::size_t const size = size_;
2392
+ for (std::size_t i = 0; i != size; ++i) {
2227
2393
  label_t label;
2228
2394
  dim_t dim;
2229
2395
  level_t level;
@@ -2238,11 +2404,15 @@ class index_gt {
2238
2404
  return result;
2239
2405
 
2240
2406
  std::size_t node_bytes = node_bytes_(dim, level);
2241
- node_t node = node_malloc_(label, {nullptr, dim}, level, true);
2407
+ node_t node = node_malloc_(dim, level);
2408
+ node.label(label);
2409
+ node.dim(dim);
2410
+ node.level(level);
2242
2411
  read_chunk(node.tape() + node_head_bytes_(), node_bytes - node_head_bytes_());
2243
2412
  if (result.error)
2244
2413
  return result;
2245
2414
  nodes_[i] = node;
2415
+ progress(i, size);
2246
2416
  }
2247
2417
 
2248
2418
  std::fclose(file);
@@ -2255,7 +2425,8 @@ class index_gt {
2255
2425
  * @b without copying the vectors and neighbors lists into RAM.
2256
2426
  * Available on Linux, MacOS, but @b not on Windows.
2257
2427
  */
2258
- serialization_result_t view(char const* file_path) noexcept {
2428
+ template <typename progress_at = dummy_progress_t>
2429
+ serialization_result_t view(char const* file_path, progress_at&& progress = {}) noexcept {
2259
2430
  serialization_result_t result;
2260
2431
 
2261
2432
  #if defined(USEARCH_DEFINED_WINDOWS)
@@ -2337,16 +2508,18 @@ class index_gt {
2337
2508
  }
2338
2509
 
2339
2510
  // Locate every node packed into file
2340
- std::size_t progress = sizeof(file_header_t);
2341
- for (std::size_t i = 0; i != size_; ++i) {
2342
- byte_t* tape = (byte_t*)(file + progress);
2511
+ std::size_t progress_bytes = sizeof(file_header_t);
2512
+ std::size_t const size = size_;
2513
+ for (std::size_t i = 0; i != size; ++i) {
2514
+ byte_t* tape = (byte_t*)(file + progress_bytes);
2343
2515
  dim_t dim = misaligned_load<dim_t>(tape + sizeof(label_t));
2344
2516
  level_t level = misaligned_load<level_t>(tape + sizeof(label_t) + sizeof(dim_t));
2345
2517
 
2346
2518
  std::size_t node_bytes = node_bytes_(dim, level);
2347
2519
  std::size_t node_vector_bytes = dim * sizeof(scalar_t);
2348
2520
  nodes_[i] = node_t{tape, (scalar_t*)(tape + node_bytes - node_vector_bytes)};
2349
- progress += node_bytes;
2521
+ progress_bytes += node_bytes;
2522
+ progress(i, size);
2350
2523
  }
2351
2524
 
2352
2525
  return {};
@@ -2354,7 +2527,224 @@ class index_gt {
2354
2527
 
2355
2528
  #pragma endregion
2356
2529
 
2530
+ struct join_result_t {
2531
+ error_t error{};
2532
+ std::size_t intersection_size{};
2533
+ std::size_t engagements{};
2534
+ std::size_t cycles{};
2535
+ std::size_t measurements{};
2536
+
2537
+ explicit operator bool() const noexcept { return !error; }
2538
+ join_result_t failed(error_t message) noexcept {
2539
+ error = std::move(message);
2540
+ return std::move(*this);
2541
+ }
2542
+ };
2543
+
2544
+ /**
2545
+ * @brief Adapts the Male-Optimal Stable Marriage algorithm for unequal sets
2546
+ * to perform fast one-to-one matching between two large collections
2547
+ * of vectors, using approximate nearest neighbors search.
2548
+ */
2549
+ template < //
2550
+ typename first_to_second_at = dummy_label_to_label_mapping_t, //
2551
+ typename second_to_first_at = dummy_label_to_label_mapping_t, //
2552
+ typename executor_at = dummy_executor_t, //
2553
+ typename progress_at = dummy_progress_t //
2554
+ >
2555
+ static join_result_t join( //
2556
+ index_gt const& first, index_gt const& second, //
2557
+ join_config_t config = {}, //
2558
+ first_to_second_at&& first_to_second = first_to_second_at{}, //
2559
+ second_to_first_at&& second_to_first = second_to_first_at{}, //
2560
+ executor_at&& executor = executor_at{}, //
2561
+ progress_at&& progress = progress_at{}) noexcept {
2562
+
2563
+ if (second.size() < first.size())
2564
+ return join_small_and_big_( //
2565
+ second, first, //
2566
+ config, //
2567
+ std::forward<second_to_first_at>(second_to_first), //
2568
+ std::forward<first_to_second_at>(first_to_second), //
2569
+ std::forward<executor_at>(executor), //
2570
+ std::forward<progress_at>(progress));
2571
+ else
2572
+ return join_small_and_big_( //
2573
+ first, second, //
2574
+ config, //
2575
+ std::forward<first_to_second_at>(first_to_second), //
2576
+ std::forward<second_to_first_at>(second_to_first), //
2577
+ std::forward<executor_at>(executor), //
2578
+ std::forward<progress_at>(progress));
2579
+ }
2580
+
2357
2581
  private:
2582
+ template <typename first_to_second_at, typename second_to_first_at, typename executor_at, typename progress_at>
2583
+ static join_result_t join_small_and_big_( //
2584
+ index_gt const& men, index_gt const& women, //
2585
+ join_config_t config, //
2586
+ first_to_second_at&& man_to_woman, //
2587
+ second_to_first_at&& woman_to_man, //
2588
+ executor_at&& executor, //
2589
+ progress_at&& progress) noexcept {
2590
+
2591
+ join_result_t result;
2592
+
2593
+ // Sanity checks and argument validation:
2594
+ if (&men == &women)
2595
+ return result.failed("Can't join with itself, consider copying");
2596
+ if (config.max_proposals == 0)
2597
+ config.max_proposals = std::log(men.size()) + executor.size();
2598
+ config.max_proposals = (std::min)(men.size(), config.max_proposals);
2599
+
2600
+ using proposals_count_t = std::uint16_t;
2601
+ using ids_allocator_t = typename allocator_traits_t::template rebind_alloc<id_t>;
2602
+ allocator_t& alloc = men.allocator_;
2603
+
2604
+ // Create an atomic queue, as a ring structure, from/to which
2605
+ // free men will be added/pulled.
2606
+ std::mutex free_men_mutex{};
2607
+ ring_gt<id_t, ids_allocator_t> free_men;
2608
+ free_men.resize(men.size());
2609
+ for (std::size_t i = 0; i != men.size(); ++i)
2610
+ free_men.push(static_cast<id_t>(i));
2611
+
2612
+ // We are gonna need some temporary memory.
2613
+ proposals_count_t* proposal_counts = (proposals_count_t*)alloc.allocate(sizeof(proposals_count_t) * men.size());
2614
+ id_t* man_to_woman_ids = (id_t*)alloc.allocate(sizeof(id_t) * men.size());
2615
+ id_t* woman_to_man_ids = (id_t*)alloc.allocate(sizeof(id_t) * women.size());
2616
+ if (!proposal_counts || !man_to_woman_ids || !woman_to_man_ids)
2617
+ return result.failed("Can't temporary mappings");
2618
+
2619
+ id_t missing_id;
2620
+ std::memset((void*)&missing_id, 0xFF, sizeof(id_t));
2621
+ std::memset((void*)man_to_woman_ids, 0xFF, sizeof(id_t) * men.size());
2622
+ std::memset((void*)woman_to_man_ids, 0xFF, sizeof(id_t) * women.size());
2623
+ std::memset(proposal_counts, 0, sizeof(proposals_count_t) * men.size());
2624
+
2625
+ // Define locks, to limit concurrent accesses to `man_to_woman_ids` and `woman_to_man_ids`.
2626
+ visits_bitset_t men_locks, women_locks;
2627
+ if (!men_locks.resize(men.size()) || !women_locks.resize(women.size()))
2628
+ return result.failed("Can't allocate locks");
2629
+
2630
+ // Accumulate statistics from all the contexts,
2631
+ // to have a baseline to compare with, by the time the `join` is finished.
2632
+ std::size_t old_measurements{};
2633
+ std::size_t old_cycles{};
2634
+ for (std::size_t thread_idx = 0; thread_idx != executor.size(); ++thread_idx) {
2635
+ old_measurements += women.contexts_[thread_idx].measurements_count;
2636
+ old_cycles += women.contexts_[thread_idx].iteration_cycles;
2637
+ }
2638
+ std::atomic<std::size_t> rounds{0};
2639
+ std::atomic<std::size_t> engagements{0};
2640
+
2641
+ // Concurrently process all the men
2642
+ executor.execute_bulk([&](std::size_t thread_idx) {
2643
+ context_t& context = women.contexts_[thread_idx];
2644
+ search_config_t search_config;
2645
+ search_config.expansion = config.expansion;
2646
+ search_config.exact = config.exact;
2647
+ search_config.thread = thread_idx;
2648
+ id_t free_man_id;
2649
+
2650
+ // While there exist a free man who still has a woman to propose to.
2651
+ while (true) {
2652
+ std::size_t passed_rounds = 0;
2653
+ std::size_t total_rounds = 0;
2654
+ {
2655
+ std::unique_lock<std::mutex> pop_lock(free_men_mutex);
2656
+ if (!free_men.try_pop(free_man_id))
2657
+ // Primary exit path, we have exhausted the list of candidates
2658
+ break;
2659
+ passed_rounds = ++rounds;
2660
+ total_rounds = passed_rounds + free_men.size();
2661
+ }
2662
+ progress(passed_rounds, total_rounds);
2663
+ while (men_locks.atomic_set(free_man_id))
2664
+ ;
2665
+
2666
+ node_t free_man = men.node_with_id_(free_man_id);
2667
+ proposals_count_t& free_man_proposals = proposal_counts[free_man_id];
2668
+ if (free_man_proposals >= config.max_proposals)
2669
+ continue;
2670
+
2671
+ // Find the closest woman, to whom this man hasn't proposed yet.
2672
+ free_man_proposals++;
2673
+ search_result_t candidates = women.search(free_man.vector_view(), free_man_proposals, search_config);
2674
+ if (!candidates) {
2675
+ // TODO:
2676
+ }
2677
+
2678
+ match_t match = candidates.back();
2679
+ member_cref_t woman = match.member;
2680
+ while (women_locks.atomic_set(woman.id))
2681
+ ;
2682
+
2683
+ id_t husband_id = woman_to_man_ids[woman.id];
2684
+ bool woman_is_free = husband_id == missing_id;
2685
+ if (woman_is_free) {
2686
+ // Engagement
2687
+ man_to_woman_ids[free_man_id] = woman.id;
2688
+ woman_to_man_ids[woman.id] = free_man_id;
2689
+ engagements++;
2690
+ } else {
2691
+ distance_t distance_from_husband = context.measure(woman.vector, men.node_with_id_(husband_id));
2692
+ distance_t distance_from_candidate = match.distance;
2693
+ if (distance_from_husband > distance_from_candidate) {
2694
+ // Break-up
2695
+ while (men_locks.atomic_set(husband_id))
2696
+ ;
2697
+ man_to_woman_ids[husband_id] = missing_id;
2698
+ men_locks.atomic_reset(husband_id);
2699
+
2700
+ // New Engagement
2701
+ man_to_woman_ids[free_man_id] = woman.id;
2702
+ woman_to_man_ids[woman.id] = free_man_id;
2703
+ engagements++;
2704
+
2705
+ std::unique_lock<std::mutex> push_lock(free_men_mutex);
2706
+ free_men.push(husband_id);
2707
+ } else {
2708
+ std::unique_lock<std::mutex> push_lock(free_men_mutex);
2709
+ free_men.push(free_man_id);
2710
+ }
2711
+ }
2712
+
2713
+ men_locks.atomic_reset(free_man_id);
2714
+ women_locks.atomic_reset(woman.id);
2715
+ }
2716
+ });
2717
+
2718
+ // Export the IDs into labels:
2719
+ std::size_t intersection_size = 0;
2720
+ for (std::size_t i = 0; i != men.size(); ++i) {
2721
+ id_t woman_id = man_to_woman_ids[i];
2722
+ if (woman_id != missing_id) {
2723
+ label_t man = men.node_with_id_(i).label();
2724
+ label_t woman = women.node_with_id_(woman_id).label();
2725
+ man_to_woman[man] = woman;
2726
+ woman_to_man[woman] = man;
2727
+ intersection_size++;
2728
+ }
2729
+ }
2730
+
2731
+ // Deallocate memory
2732
+ alloc.deallocate((byte_t*)proposal_counts, sizeof(proposals_count_t) * men.size());
2733
+ alloc.deallocate((byte_t*)man_to_woman_ids, sizeof(id_t) * men.size());
2734
+ alloc.deallocate((byte_t*)woman_to_man_ids, sizeof(id_t) * women.size());
2735
+
2736
+ // Export stats
2737
+ result.engagements = engagements;
2738
+ result.intersection_size = intersection_size;
2739
+ for (std::size_t thread_idx = 0; thread_idx != executor.size(); ++thread_idx) {
2740
+ result.measurements += women.contexts_[thread_idx].measurements_count;
2741
+ result.cycles += women.contexts_[thread_idx].iteration_cycles;
2742
+ }
2743
+ result.measurements -= old_measurements;
2744
+ result.cycles -= old_cycles;
2745
+ return result;
2746
+ }
2747
+
2358
2748
  void reset_view_() noexcept {
2359
2749
  if (!viewed_file_)
2360
2750
  return;
@@ -2384,51 +2774,80 @@ class index_gt {
2384
2774
  return node_head_bytes_() + pre_.neighbors_base_bytes + pre_.neighbors_bytes * level + sizeof(scalar_t) * dim;
2385
2775
  }
2386
2776
 
2387
- inline bool node_stored_(node_t node) const noexcept {
2777
+ using span_bytes_t = span_gt<byte_t>;
2778
+ struct node_bytes_split_t {
2779
+ span_bytes_t tape{};
2780
+ span_bytes_t vector{};
2781
+
2782
+ node_bytes_split_t() {}
2783
+ node_bytes_split_t(span_bytes_t tape, span_bytes_t vector) noexcept : tape(tape), vector(vector) {}
2784
+
2785
+ std::size_t memory_usage() const noexcept { return tape.size() + vector.size(); }
2786
+ bool colocated() const noexcept { return tape.end() == vector.begin(); }
2787
+ operator node_t() const noexcept { return node_t{tape.begin(), reinterpret_cast<scalar_t*>(vector.begin())}; }
2788
+ };
2789
+
2790
+ inline node_bytes_split_t node_bytes_split_(node_t node) const noexcept {
2388
2791
  std::size_t levels_bytes = pre_.neighbors_base_bytes + pre_.neighbors_bytes * node.level();
2389
- return (node.tape() + node_head_bytes_() + levels_bytes) == (byte_t*)node.vector();
2792
+ std::size_t bytes_in_tape = node_head_bytes_() + levels_bytes;
2793
+ return {{node.tape(), bytes_in_tape}, {(byte_t*)node.vector(), node_vector_bytes_(node)}};
2390
2794
  }
2391
2795
 
2392
2796
  inline std::size_t node_vector_bytes_(dim_t dim) const noexcept { return dim * sizeof(scalar_t); }
2393
2797
  inline std::size_t node_vector_bytes_(node_t node) const noexcept { return node_vector_bytes_(node.dim()); }
2394
2798
 
2395
- void node_free_(std::size_t id) noexcept {
2396
-
2397
- if (viewed_file_)
2398
- return;
2399
-
2400
- node_t& node = nodes_[id];
2401
- std::size_t node_bytes = node_bytes_(node) - node_vector_bytes_(node) * !node_stored_(node);
2402
- point_allocator_.deallocate(node.tape(), node_bytes);
2403
- node = node_t{};
2404
- }
2405
-
2406
- node_t node_malloc_(label_t label, vector_view_t vector, level_t level, bool store_vector) noexcept {
2799
+ node_bytes_split_t node_malloc_(dim_t dims_to_store, level_t level) noexcept {
2407
2800
 
2408
- std::size_t dim = vector.size();
2409
- std::size_t stored_vector_bytes = node_vector_bytes_(static_cast<dim_t>(dim)) * std::size_t(store_vector);
2410
- std::size_t node_bytes = node_bytes_(static_cast<dim_t>(dim), level) -
2411
- node_vector_bytes_(static_cast<dim_t>(dim)) * std::size_t(!store_vector);
2801
+ std::size_t vector_bytes = node_vector_bytes_(dims_to_store);
2802
+ std::size_t node_bytes = node_bytes_(dims_to_store, level);
2803
+ std::size_t non_vector_bytes = node_bytes - vector_bytes;
2412
2804
 
2413
2805
  byte_t* data = (byte_t*)point_allocator_.allocate(node_bytes);
2414
2806
  if (!data)
2415
- return {};
2416
-
2417
- std::memset(data, 0, node_bytes);
2418
- if (vector.data())
2419
- std::memcpy(data + node_bytes - stored_vector_bytes, vector.data(), stored_vector_bytes);
2420
-
2421
- scalar_t* scalars = store_vector //
2422
- ? (scalar_t*)(data + node_bytes - stored_vector_bytes)
2423
- : (scalar_t*)(vector.data());
2807
+ return node_bytes_split_t{};
2808
+ return {{data, non_vector_bytes}, {data + non_vector_bytes, vector_bytes}};
2809
+ }
2424
2810
 
2425
- node_t node{data, scalars};
2811
+ node_t node_make_(label_t label, vector_view_t vector, level_t level, bool store_vector) noexcept {
2812
+ node_bytes_split_t node_bytes = node_malloc_(vector.size() * store_vector, level);
2813
+ if (store_vector) {
2814
+ std::memset(node_bytes.tape.data(), 0, node_bytes.tape.size());
2815
+ std::memcpy(node_bytes.vector.data(), vector.data(), node_bytes.vector.size());
2816
+ } else {
2817
+ std::memset(node_bytes.tape.data(), 0, node_bytes.memory_usage());
2818
+ }
2819
+ node_t node = node_bytes;
2426
2820
  node.label(label);
2427
- node.dim(static_cast<dim_t>(dim));
2821
+ node.dim(static_cast<dim_t>(vector.size()));
2428
2822
  node.level(level);
2429
2823
  return node;
2430
2824
  }
2431
2825
 
2826
+ node_t node_make_copy_(node_bytes_split_t old_bytes) noexcept {
2827
+ if (old_bytes.colocated()) {
2828
+ byte_t* data = (byte_t*)point_allocator_.allocate(old_bytes.memory_usage());
2829
+ std::memcpy(data, old_bytes.tape.data(), old_bytes.memory_usage());
2830
+ return node_t{data, reinterpret_cast<scalar_t*>(data + old_bytes.tape.size())};
2831
+ } else {
2832
+ node_t old_node = old_bytes;
2833
+ node_bytes_split_t node_bytes = node_malloc_(old_node.vector_view().size(), old_node.level());
2834
+ std::memcpy(node_bytes.tape.data(), old_bytes.tape.data(), old_bytes.tape.size());
2835
+ std::memcpy(node_bytes.vector.data(), old_bytes.vector.data(), old_bytes.vector.size());
2836
+ return node_bytes;
2837
+ }
2838
+ }
2839
+
2840
+ void node_free_(std::size_t id) noexcept {
2841
+
2842
+ if (viewed_file_)
2843
+ return;
2844
+
2845
+ node_t& node = nodes_[id];
2846
+ std::size_t node_bytes = node_bytes_(node) - node_vector_bytes_(node) * !node_bytes_split_(node).colocated();
2847
+ point_allocator_.deallocate(node.tape(), node_bytes);
2848
+ node = node_t{};
2849
+ }
2850
+
2432
2851
  inline node_t node_with_id_(std::size_t idx) const noexcept { return nodes_[idx]; }
2433
2852
  inline neighbors_ref_t neighbors_base_(node_t node) const noexcept { return {node.neighbors_tape()}; }
2434
2853