usearch 2.16.8 → 2.16.9

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.
@@ -1,3 +1,4 @@
1
+ #include <array> // `std::array`
1
2
  #include <cmath> // `std::sqrt`
2
3
  #include <cstdlib> // `std::aligned_alloc`
3
4
  #include <cstring> // `std::memcpy`
@@ -34,33 +35,33 @@ constexpr simsimd_distance_t signaling_distance = std::numeric_limits<simsimd_di
34
35
  /// For sub-byte data types
35
36
  constexpr std::size_t dense_dimensions = 1536;
36
37
  /// Has quadratic impact on the number of operations
37
- constexpr std::size_t curved_dimensions = 128;
38
+ constexpr std::size_t curved_dimensions = 8;
38
39
 
39
40
  namespace bm = benchmark;
40
41
 
41
42
  // clang-format off
42
- template <simsimd_datatype_t> struct datatype_enum_to_type_gt { using value_t = void; };
43
- template <> struct datatype_enum_to_type_gt<simsimd_datatype_f64_k> { using value_t = simsimd_f64_t; };
44
- template <> struct datatype_enum_to_type_gt<simsimd_datatype_f32_k> { using value_t = simsimd_f32_t; };
45
- template <> struct datatype_enum_to_type_gt<simsimd_datatype_f16_k> { using value_t = simsimd_f16_t; };
46
- template <> struct datatype_enum_to_type_gt<simsimd_datatype_bf16_k> { using value_t = simsimd_bf16_t; };
47
- template <> struct datatype_enum_to_type_gt<simsimd_datatype_f64c_k> { using value_t = simsimd_f64_t; };
48
- template <> struct datatype_enum_to_type_gt<simsimd_datatype_f32c_k> { using value_t = simsimd_f32_t; };
49
- template <> struct datatype_enum_to_type_gt<simsimd_datatype_f16c_k> { using value_t = simsimd_f16_t; };
50
- template <> struct datatype_enum_to_type_gt<simsimd_datatype_bf16c_k> { using value_t = simsimd_bf16_t; };
51
- template <> struct datatype_enum_to_type_gt<simsimd_datatype_b8_k> { using value_t = simsimd_b8_t; };
52
- template <> struct datatype_enum_to_type_gt<simsimd_datatype_i8_k> { using value_t = simsimd_i8_t; };
53
- template <> struct datatype_enum_to_type_gt<simsimd_datatype_u8_k> { using value_t = simsimd_u8_t; };
54
- template <> struct datatype_enum_to_type_gt<simsimd_datatype_i16_k> { using value_t = simsimd_i16_t; };
55
- template <> struct datatype_enum_to_type_gt<simsimd_datatype_u16_k> { using value_t = simsimd_u16_t; };
56
- template <> struct datatype_enum_to_type_gt<simsimd_datatype_i32_k> { using value_t = simsimd_i32_t; };
57
- template <> struct datatype_enum_to_type_gt<simsimd_datatype_u32_k> { using value_t = simsimd_u32_t; };
58
- template <> struct datatype_enum_to_type_gt<simsimd_datatype_i64_k> { using value_t = simsimd_i64_t; };
59
- template <> struct datatype_enum_to_type_gt<simsimd_datatype_u64_k> { using value_t = simsimd_u64_t; };
43
+ template <simsimd_datatype_t> struct datatype_enum_to_type_gt { using value_t = void; using scalar_t = void; static constexpr std::size_t components_k = 1; };
44
+ template <> struct datatype_enum_to_type_gt<simsimd_datatype_f64_k> { using value_t = simsimd_f64_t; using scalar_t = simsimd_f64_t; static constexpr std::size_t components_k = 1; };
45
+ template <> struct datatype_enum_to_type_gt<simsimd_datatype_f32_k> { using value_t = simsimd_f32_t; using scalar_t = simsimd_f32_t; static constexpr std::size_t components_k = 1; };
46
+ template <> struct datatype_enum_to_type_gt<simsimd_datatype_f16_k> { using value_t = simsimd_f16_t; using scalar_t = simsimd_f16_t; static constexpr std::size_t components_k = 1; };
47
+ template <> struct datatype_enum_to_type_gt<simsimd_datatype_bf16_k> { using value_t = simsimd_bf16_t; using scalar_t = simsimd_bf16_t; static constexpr std::size_t components_k = 1; };
48
+ template <> struct datatype_enum_to_type_gt<simsimd_datatype_f64c_k> { using value_t = simsimd_f64c_t; using scalar_t = simsimd_f64_t; static constexpr std::size_t components_k = 2; };
49
+ template <> struct datatype_enum_to_type_gt<simsimd_datatype_f32c_k> { using value_t = simsimd_f32c_t; using scalar_t = simsimd_f32_t; static constexpr std::size_t components_k = 2; };
50
+ template <> struct datatype_enum_to_type_gt<simsimd_datatype_f16c_k> { using value_t = simsimd_f16c_t; using scalar_t = simsimd_f16_t; static constexpr std::size_t components_k = 2; };
51
+ template <> struct datatype_enum_to_type_gt<simsimd_datatype_bf16c_k> { using value_t = simsimd_bf16c_t; using scalar_t = simsimd_bf16_t; static constexpr std::size_t components_k = 2; };
52
+ template <> struct datatype_enum_to_type_gt<simsimd_datatype_b8_k> { using value_t = simsimd_b8_t; using scalar_t = simsimd_b8_t; static constexpr std::size_t components_k = 1; };
53
+ template <> struct datatype_enum_to_type_gt<simsimd_datatype_i8_k> { using value_t = simsimd_i8_t; using scalar_t = simsimd_i8_t; static constexpr std::size_t components_k = 1; };
54
+ template <> struct datatype_enum_to_type_gt<simsimd_datatype_u8_k> { using value_t = simsimd_u8_t; using scalar_t = simsimd_u8_t; static constexpr std::size_t components_k = 1; };
55
+ template <> struct datatype_enum_to_type_gt<simsimd_datatype_i16_k> { using value_t = simsimd_i16_t; using scalar_t = simsimd_i16_t; static constexpr std::size_t components_k = 1; };
56
+ template <> struct datatype_enum_to_type_gt<simsimd_datatype_u16_k> { using value_t = simsimd_u16_t; using scalar_t = simsimd_u16_t; static constexpr std::size_t components_k = 1; };
57
+ template <> struct datatype_enum_to_type_gt<simsimd_datatype_i32_k> { using value_t = simsimd_i32_t; using scalar_t = simsimd_i32_t; static constexpr std::size_t components_k = 1; };
58
+ template <> struct datatype_enum_to_type_gt<simsimd_datatype_u32_k> { using value_t = simsimd_u32_t; using scalar_t = simsimd_u32_t; static constexpr std::size_t components_k = 1; };
59
+ template <> struct datatype_enum_to_type_gt<simsimd_datatype_i64_k> { using value_t = simsimd_i64_t; using scalar_t = simsimd_i64_t; static constexpr std::size_t components_k = 1; };
60
+ template <> struct datatype_enum_to_type_gt<simsimd_datatype_u64_k> { using value_t = simsimd_u64_t; using scalar_t = simsimd_u64_t; static constexpr std::size_t components_k = 1; };
60
61
  // clang-format on
61
62
 
62
63
  template <std::size_t multiple>
63
- std::size_t divide_round_up(std::size_t n) {
64
+ constexpr std::size_t divide_round_up(std::size_t n) {
64
65
  return ((n + multiple - 1) / multiple) * multiple;
65
66
  }
66
67
 
@@ -70,8 +71,11 @@ std::size_t divide_round_up(std::size_t n) {
70
71
  */
71
72
  template <simsimd_datatype_t datatype_ak>
72
73
  struct vector_gt {
73
- using scalar_t = typename datatype_enum_to_type_gt<datatype_ak>::value_t;
74
- using compressed16_t = unsigned short;
74
+ using datatype_reflection_t = datatype_enum_to_type_gt<datatype_ak>;
75
+ using scalar_t = typename datatype_reflection_t::scalar_t;
76
+ using value_t = typename datatype_reflection_t::value_t;
77
+ static constexpr std::size_t components_k = datatype_reflection_t::components_k;
78
+
75
79
  static constexpr bool is_integral =
76
80
  datatype_ak == datatype_ak == simsimd_datatype_b8_k || //
77
81
  datatype_ak == simsimd_datatype_i8_k || datatype_ak == simsimd_datatype_u8_k || //
@@ -80,49 +84,54 @@ struct vector_gt {
80
84
  datatype_ak == simsimd_datatype_i64_k || datatype_ak == simsimd_datatype_u64_k;
81
85
  static constexpr std::size_t cacheline_length = 64;
82
86
 
83
- scalar_t *buffer_ = nullptr;
84
- std::size_t dimensions_ = 0;
87
+ value_t *values_ptr_ = nullptr;
88
+ std::size_t values_count_ = 0;
85
89
 
86
90
  vector_gt() = default;
87
- vector_gt(std::size_t dimensions) noexcept(false)
88
- : dimensions_(dimensions),
89
- buffer_(static_cast<scalar_t *>(
90
- std::aligned_alloc(cacheline_length, divide_round_up<cacheline_length>(dimensions * sizeof(scalar_t))))) {
91
- if (!buffer_) throw std::bad_alloc();
91
+ vector_gt(std::size_t values_count) noexcept(false)
92
+ : values_count_(values_count),
93
+ values_ptr_(static_cast<value_t *>(std::aligned_alloc(
94
+ cacheline_length, divide_round_up<cacheline_length>(values_count * sizeof(value_t))))) {
95
+ if (!values_ptr_) throw std::bad_alloc();
92
96
  }
93
97
 
94
- ~vector_gt() noexcept { std::free(buffer_); }
98
+ ~vector_gt() noexcept { std::free(values_ptr_); }
95
99
 
96
- vector_gt(vector_gt const &other) : vector_gt(other.dimensions()) {
97
- std::memcpy(buffer_, other.buffer_, divide_round_up<cacheline_length>(dimensions_ * sizeof(scalar_t)));
100
+ vector_gt(vector_gt const &other) : vector_gt(other.size()) {
101
+ std::memcpy(values_ptr_, other.values_ptr_, divide_round_up<cacheline_length>(values_count_ * sizeof(value_t)));
98
102
  }
99
103
  vector_gt &operator=(vector_gt const &other) {
100
104
  if (this != &other) {
101
- if (dimensions_ != other.dimensions()) {
102
- std::free(buffer_);
103
- dimensions_ = other.dimensions();
104
- buffer_ = static_cast<scalar_t *>(std::aligned_alloc(
105
- cacheline_length, divide_round_up<cacheline_length>(dimensions_ * sizeof(scalar_t))));
106
- if (!buffer_) throw std::bad_alloc();
105
+ if (values_count_ != other.size()) {
106
+ std::free(values_ptr_);
107
+ values_count_ = other.size();
108
+ values_ptr_ = static_cast<value_t *>(std::aligned_alloc(
109
+ cacheline_length, divide_round_up<cacheline_length>(values_count_ * sizeof(value_t))));
110
+ if (!values_ptr_) throw std::bad_alloc();
107
111
  }
108
- std::memcpy(buffer_, other.buffer_, divide_round_up<cacheline_length>(dimensions_ * sizeof(scalar_t)));
112
+ std::memcpy(values_ptr_, other.values_ptr_,
113
+ divide_round_up<cacheline_length>(values_count_ * sizeof(value_t)));
109
114
  }
110
115
  return *this;
111
116
  }
112
117
 
113
- std::size_t dimensions() const noexcept { return dimensions_; }
118
+ value_t *data() noexcept { return values_ptr_; }
119
+ value_t const *data() const noexcept { return values_ptr_; }
120
+ std::size_t size() const noexcept { return values_count_; }
114
121
  std::size_t size_bytes() const noexcept {
115
- return divide_round_up<cacheline_length>(dimensions_ * sizeof(scalar_t));
122
+ return divide_round_up<cacheline_length>(values_count_ * sizeof(value_t));
116
123
  }
117
- scalar_t *data() noexcept { return buffer_; }
118
- scalar_t const *data() const noexcept { return buffer_; }
124
+
125
+ scalar_t *data_scalars() noexcept { return reinterpret_cast<scalar_t *>(data()); }
126
+ scalar_t const *data_scalars() const noexcept { return reinterpret_cast<scalar_t *>(data()); }
127
+ std::size_t size_scalars() const noexcept { return size() * components_k; }
119
128
 
120
129
  /**
121
130
  * @brief Broadcast a scalar value to all elements of the vector.
122
131
  * @param v The scalar value to broadcast.
123
132
  */
124
133
  void set(scalar_t v) noexcept {
125
- for (std::size_t i = 0; i != dimensions_; ++i) buffer_[i] = v;
134
+ for (std::size_t i = 0; i != size_scalars(); ++i) data_scalars()[i] = v;
126
135
  }
127
136
 
128
137
  /**
@@ -193,24 +202,24 @@ struct vector_gt {
193
202
  if constexpr (is_integral) {
194
203
  std::uniform_int_distribution<scalar_t> distribution(std::numeric_limits<scalar_t>::min(),
195
204
  std::numeric_limits<scalar_t>::max());
196
- for (std::size_t i = 0; i != dimensions_; ++i) { buffer_[i] = distribution(generator); }
205
+ for (std::size_t i = 0; i != size_scalars(); ++i) { data_scalars()[i] = distribution(generator); }
197
206
  }
198
207
  else {
199
208
  // Using non-uniform distribution helps detect tail errors
200
209
  std::normal_distribution<double> distribution(0.1, 1.0);
201
210
  double squared_sum = 0.0;
202
- for (std::size_t i = 0; i != dimensions_; ++i) {
211
+ for (std::size_t i = 0; i != size_scalars(); ++i) {
203
212
  double a_i = distribution(generator);
204
213
  squared_sum += a_i * a_i;
205
- compress(a_i, buffer_[i]);
214
+ compress(a_i, data_scalars()[i]);
206
215
  }
207
216
 
208
217
  // Normalize the vectors:
209
218
  squared_sum = std::sqrt(squared_sum);
210
- for (std::size_t i = 0; i != dimensions_; ++i) {
211
- compress(uncompress(buffer_[i]) / squared_sum, buffer_[i]);
219
+ for (std::size_t i = 0; i != size_scalars(); ++i) {
220
+ compress(uncompress(data_scalars()[i]) / squared_sum, data_scalars()[i]);
212
221
  // Zero out NaNs
213
- if (std::isnan(uncompress(buffer_[i]))) buffer_[i] = 0;
222
+ if (std::isnan(uncompress(data_scalars()[i]))) data_scalars()[i] = 0;
214
223
  }
215
224
  }
216
225
  }
@@ -227,7 +236,7 @@ struct vectors_pair_gt {
227
236
 
228
237
  vectors_pair_gt() noexcept = default;
229
238
  vectors_pair_gt(std::size_t dimensions) noexcept : a(dimensions), b(dimensions) {}
230
- vectors_pair_gt(std::size_t dimensions_a, std::size_t dimensions_b) noexcept : a(dimensions_a), b(dimensions_b) {}
239
+ vectors_pair_gt(std::size_t size_a, std::size_t size_b) noexcept : a(size_a), b(size_b) {}
231
240
  vectors_pair_gt(vectors_pair_gt const &other) noexcept(false) : a(other.a), b(other.b) {}
232
241
  vectors_pair_gt &operator=(vectors_pair_gt const &other) noexcept(false) {
233
242
  if (this != &other) a = other.a, b = other.b;
@@ -254,14 +263,14 @@ void measure_dense(bm::State &state, metric_at metric, metric_at baseline, std::
254
263
  // Output for real vectors have a single dimensions.
255
264
  // Output for complex vectors have two dimensions.
256
265
  simsimd_distance_t results[2] = {signaling_distance, signaling_distance};
257
- baseline(pair.a.data(), pair.b.data(), pair.a.dimensions(), &results[0]);
266
+ baseline(pair.a.data(), pair.b.data(), pair.a.size(), &results[0]);
258
267
  return results[0];
259
268
  };
260
269
  auto call_contender = [&](pair_t &pair) -> double {
261
270
  // Output for real vectors have a single dimensions.
262
271
  // Output for complex vectors have two dimensions.
263
272
  simsimd_distance_t results[2] = {signaling_distance, signaling_distance};
264
- metric(pair.a.data(), pair.b.data(), pair.a.dimensions(), &results[0]);
273
+ metric(pair.a.data(), pair.b.data(), pair.a.size(), &results[0]);
265
274
  return results[0];
266
275
  };
267
276
 
@@ -319,14 +328,14 @@ void measure_curved(bm::State &state, metric_at metric, metric_at baseline, std:
319
328
  using vector_t = typename pair_at::vector_t;
320
329
 
321
330
  auto call_baseline = [&](pair_t const &pair, vector_t const &tensor) -> double {
322
- simsimd_distance_t result = signaling_distance;
323
- baseline(pair.a.data(), pair.b.data(), tensor.data(), pair.a.dimensions(), &result);
324
- return result;
331
+ simsimd_distance_t results[2] = {signaling_distance, 0};
332
+ baseline(pair.a.data(), pair.b.data(), tensor.data(), pair.a.size(), &results[0]);
333
+ return results[0] + results[1];
325
334
  };
326
335
  auto call_contender = [&](pair_t const &pair, vector_t const &tensor) -> double {
327
- simsimd_distance_t result = signaling_distance;
328
- metric(pair.a.data(), pair.b.data(), tensor.data(), pair.a.dimensions(), &result);
329
- return result;
336
+ simsimd_distance_t results[2] = {signaling_distance, 0};
337
+ metric(pair.a.data(), pair.b.data(), tensor.data(), pair.a.size(), &results[0]);
338
+ return results[0] + results[1];
330
339
  };
331
340
 
332
341
  // Let's average the distance results over many pairs.
@@ -379,13 +388,13 @@ void measure_curved(bm::State &state, metric_at metric, metric_at baseline, std:
379
388
  * @param state The benchmark state object provided by Google Benchmark.
380
389
  * @param metric The metric function to benchmark.
381
390
  * @param baseline The baseline function to compare against.
382
- * @param dimensions_a The number of dimensions in the smaller vector.
383
- * @param dimensions_b The number of dimensions in the larger vector.
391
+ * @param size_a The number of elements in the smaller vector.
392
+ * @param size_b The number of elements in the larger vector.
384
393
  * @param intersection_size The expected number of common scalars between the vectors.
385
394
  */
386
395
  template <typename pair_at, typename metric_at = void>
387
- void measure_sparse(bm::State &state, metric_at metric, metric_at baseline, std::size_t dimensions_a,
388
- std::size_t dimensions_b, std::size_t intersection_size) {
396
+ void measure_sparse(bm::State &state, metric_at metric, metric_at baseline, std::size_t size_a, std::size_t size_b,
397
+ std::size_t intersection_size) {
389
398
 
390
399
  using pair_t = pair_at;
391
400
  using vector_t = typename pair_at::vector_t;
@@ -393,12 +402,12 @@ void measure_sparse(bm::State &state, metric_at metric, metric_at baseline, std:
393
402
 
394
403
  auto call_baseline = [&](pair_t &pair) -> double {
395
404
  simsimd_distance_t result = std::numeric_limits<simsimd_distance_t>::signaling_NaN();
396
- baseline(pair.a.data(), pair.b.data(), pair.a.dimensions(), pair.b.dimensions(), &result);
405
+ baseline(pair.a.data(), pair.b.data(), pair.a.size(), pair.b.size(), &result);
397
406
  return result;
398
407
  };
399
408
  auto call_contender = [&](pair_t &pair) -> double {
400
409
  simsimd_distance_t result = std::numeric_limits<simsimd_distance_t>::signaling_NaN();
401
- metric(pair.a.data(), pair.b.data(), pair.a.dimensions(), pair.b.dimensions(), &result);
410
+ metric(pair.a.data(), pair.b.data(), pair.a.size(), pair.b.size(), &result);
402
411
  return result;
403
412
  };
404
413
 
@@ -416,37 +425,37 @@ void measure_sparse(bm::State &state, metric_at metric, metric_at baseline, std:
416
425
  // We need to ensure that the intersection is of the expected size.
417
426
  std::unordered_set<scalar_t> intersection_set, unique_a, unique_b;
418
427
  intersection_set.reserve(intersection_size);
419
- unique_a.reserve(dimensions_a - intersection_size);
420
- unique_b.reserve(dimensions_b - intersection_size);
428
+ unique_a.reserve(size_a - intersection_size);
429
+ unique_b.reserve(size_b - intersection_size);
421
430
 
422
431
  for (auto &pair : pairs) {
423
- pair.a = vector_t(dimensions_a);
424
- pair.b = vector_t(dimensions_b);
432
+ pair.a = vector_t(size_a);
433
+ pair.b = vector_t(size_b);
425
434
 
426
435
  // Step 1: Generate intersection set
427
436
  intersection_set.clear();
428
437
  while (intersection_set.size() < intersection_size) intersection_set.insert(distribution(generator));
429
438
 
430
439
  unique_a.clear();
431
- while (unique_a.size() < dimensions_a - intersection_size) {
440
+ while (unique_a.size() < size_a - intersection_size) {
432
441
  scalar_t element = distribution(generator);
433
442
  if (intersection_set.find(element) == intersection_set.end()) unique_a.insert(element);
434
443
  }
435
444
 
436
445
  unique_b.clear();
437
- while (unique_b.size() < dimensions_b - intersection_size) {
446
+ while (unique_b.size() < size_b - intersection_size) {
438
447
  scalar_t element = distribution(generator);
439
448
  if (intersection_set.find(element) == intersection_set.end() && unique_a.find(element) == unique_a.end())
440
449
  unique_b.insert(element);
441
450
  }
442
451
 
443
452
  // Step 2: Merge and sort
444
- std::copy(intersection_set.begin(), intersection_set.end(), pair.a.buffer_);
445
- std::copy(intersection_set.begin(), intersection_set.end(), pair.b.buffer_);
446
- std::copy(unique_a.begin(), unique_a.end(), pair.a.buffer_ + intersection_size);
447
- std::copy(unique_b.begin(), unique_b.end(), pair.b.buffer_ + intersection_size);
448
- std::sort(pair.a.buffer_, pair.a.buffer_ + dimensions_a);
449
- std::sort(pair.b.buffer_, pair.b.buffer_ + dimensions_b);
453
+ std::copy(intersection_set.begin(), intersection_set.end(), pair.a.values_ptr_);
454
+ std::copy(intersection_set.begin(), intersection_set.end(), pair.b.values_ptr_);
455
+ std::copy(unique_a.begin(), unique_a.end(), pair.a.values_ptr_ + intersection_size);
456
+ std::copy(unique_b.begin(), unique_b.end(), pair.b.values_ptr_ + intersection_size);
457
+ std::sort(pair.a.values_ptr_, pair.a.values_ptr_ + size_a);
458
+ std::sort(pair.b.values_ptr_, pair.b.values_ptr_ + size_b);
450
459
  }
451
460
 
452
461
  // Initialize the output buffers for distance calculations.
@@ -505,15 +514,15 @@ void measure_fma(bm::State &state, kernel_at kernel, kernel_at baseline, l2_metr
505
514
 
506
515
  auto call_baseline = [&](vector_t const &a, vector_t const &b, vector_t const &c, vector_t &d) {
507
516
  if constexpr (function_args_count(kernel_at {}) == 6) {
508
- baseline(a.data(), c.data(), a.dimensions(), alpha, beta, d.data());
517
+ baseline(a.data(), c.data(), a.size(), alpha, beta, d.data());
509
518
  }
510
- else { baseline(a.data(), b.data(), c.data(), a.dimensions(), alpha, beta, d.data()); }
519
+ else { baseline(a.data(), b.data(), c.data(), a.size(), alpha, beta, d.data()); }
511
520
  };
512
521
  auto call_contender = [&](vector_t const &a, vector_t const &b, vector_t const &c, vector_t &d) {
513
522
  if constexpr (function_args_count(kernel_at {}) == 6) {
514
- kernel(a.data(), c.data(), a.dimensions(), alpha, beta, d.data());
523
+ kernel(a.data(), c.data(), a.size(), alpha, beta, d.data());
515
524
  }
516
- else { kernel(a.data(), b.data(), c.data(), a.dimensions(), alpha, beta, d.data()); }
525
+ else { kernel(a.data(), b.data(), c.data(), a.size(), alpha, beta, d.data()); }
517
526
  };
518
527
 
519
528
  // Let's average the distance results over many quads.
@@ -634,26 +643,61 @@ void dot_f64_blas(simsimd_f64_t const *a, simsimd_f64_t const *b, simsimd_size_t
634
643
  *result = cblas_ddot((int)n, a, 1, b, 1);
635
644
  }
636
645
 
637
- void dot_f32c_blas(simsimd_f32_t const *a, simsimd_f32_t const *b, simsimd_size_t n, simsimd_distance_t *result) {
646
+ void dot_f32c_blas(simsimd_f32c_t const *a, simsimd_f32c_t const *b, simsimd_size_t n, simsimd_distance_t *result) {
638
647
  simsimd_f32_t f32_result[2] = {0, 0};
639
- cblas_cdotu_sub((int)n / 2, a, 1, b, 1, f32_result);
648
+ cblas_cdotu_sub((int)n, (simsimd_f32_t const *)a, 1, (simsimd_f32_t const *)b, 1, f32_result);
640
649
  result[0] = f32_result[0];
641
650
  result[1] = f32_result[1];
642
651
  }
643
652
 
644
- void dot_f64c_blas(simsimd_f64_t const *a, simsimd_f64_t const *b, simsimd_size_t n, simsimd_distance_t *result) {
645
- cblas_zdotu_sub((int)n / 2, a, 1, b, 1, result);
653
+ void dot_f64c_blas(simsimd_f64c_t const *a, simsimd_f64c_t const *b, simsimd_size_t n, simsimd_distance_t *result) {
654
+ cblas_zdotu_sub((int)n, (simsimd_f64_t const *)a, 1, (simsimd_f64_t const *)b, 1, result);
646
655
  }
647
656
 
648
- void vdot_f32c_blas(simsimd_f32_t const *a, simsimd_f32_t const *b, simsimd_size_t n, simsimd_distance_t *result) {
657
+ void vdot_f32c_blas(simsimd_f32c_t const *a, simsimd_f32c_t const *b, simsimd_size_t n, simsimd_distance_t *result) {
649
658
  simsimd_f32_t f32_result[2] = {0, 0};
650
- cblas_cdotc_sub((int)n / 2, a, 1, b, 1, f32_result);
659
+ cblas_cdotc_sub((int)n, (simsimd_f32_t const *)a, 1, (simsimd_f32_t const *)b, 1, f32_result);
651
660
  result[0] = f32_result[0];
652
661
  result[1] = f32_result[1];
653
662
  }
654
663
 
655
- void vdot_f64c_blas(simsimd_f64_t const *a, simsimd_f64_t const *b, simsimd_size_t n, simsimd_distance_t *result) {
656
- cblas_zdotc_sub((int)n / 2, a, 1, b, 1, result);
664
+ void vdot_f64c_blas(simsimd_f64c_t const *a, simsimd_f64c_t const *b, simsimd_size_t n, simsimd_distance_t *result) {
665
+ cblas_zdotc_sub((int)n, (simsimd_f64_t const *)a, 1, (simsimd_f64_t const *)b, 1, result);
666
+ }
667
+
668
+ void bilinear_f32_blas(simsimd_f32_t const *a, simsimd_f32_t const *b, simsimd_f32_t const *c, simsimd_size_t n,
669
+ simsimd_distance_t *result) {
670
+ std::array<simsimd_f32_t, curved_dimensions> intermediate;
671
+ simsimd_f32_t alpha = 1.0f, beta = 0.0f;
672
+ cblas_sgemv(CblasRowMajor, CblasNoTrans, (int)n, (int)n, alpha, c, (int)n, b, 1, beta, intermediate.data(), 1);
673
+ *result = cblas_sdot((int)n, a, 1, intermediate.data(), 1);
674
+ }
675
+
676
+ void bilinear_f64_blas(simsimd_f64_t const *a, simsimd_f64_t const *b, simsimd_f64_t const *c, simsimd_size_t n,
677
+ simsimd_distance_t *result) {
678
+ std::array<simsimd_f64_t, curved_dimensions> intermediate;
679
+ simsimd_f64_t alpha = 1.0, beta = 0.0;
680
+ cblas_dgemv(CblasRowMajor, CblasNoTrans, (int)n, (int)n, alpha, c, n, b, 1, beta, intermediate.data(), 1);
681
+ *result = cblas_ddot((int)n, a, 1, intermediate.data(), 1);
682
+ }
683
+
684
+ void bilinear_f32c_blas(simsimd_f32c_t const *a, simsimd_f32c_t const *b, simsimd_f32c_t const *c, simsimd_size_t n,
685
+ simsimd_distance_t *results) {
686
+ std::array<simsimd_f32c_t, curved_dimensions> intermediate;
687
+ simsimd_f32c_t alpha = {1.0f, 0.0f}, beta = {0.0f, 0.0f};
688
+ cblas_cgemv(CblasRowMajor, CblasNoTrans, (int)n, (int)n, &alpha, c, n, b, 1, &beta, intermediate.data(), 1);
689
+ simsimd_f32_t f32_result[2] = {0, 0};
690
+ cblas_cdotu_sub((int)n, (simsimd_f32_t const *)a, 1, (simsimd_f32_t const *)intermediate.data(), 1, f32_result);
691
+ results[0] = f32_result[0];
692
+ results[1] = f32_result[1];
693
+ }
694
+
695
+ void bilinear_f64c_blas(simsimd_f64c_t const *a, simsimd_f64c_t const *b, simsimd_f64c_t const *c, simsimd_size_t n,
696
+ simsimd_distance_t *results) {
697
+ std::array<simsimd_f64c_t, curved_dimensions> intermediate;
698
+ simsimd_f64c_t alpha = {1.0, 0.0}, beta = {0.0, 0.0};
699
+ cblas_zgemv(CblasRowMajor, CblasNoTrans, (int)n, (int)n, &alpha, c, n, b, 1, &beta, intermediate.data(), 1);
700
+ cblas_zdotu_sub((int)n, (simsimd_f64_t const *)a, 1, (simsimd_f64_t const *)intermediate.data(), 1, results);
657
701
  }
658
702
 
659
703
  #endif
@@ -730,6 +774,11 @@ int main(int argc, char **argv) {
730
774
  dense_<f32c_k>("vdot_f32c_blas", vdot_f32c_blas, simsimd_vdot_f32c_accurate);
731
775
  dense_<f64c_k>("vdot_f64c_blas", vdot_f64c_blas, simsimd_vdot_f64c_serial);
732
776
 
777
+ curved_<f64_k>("bilinear_f64_blas", bilinear_f64_blas, simsimd_bilinear_f64_serial);
778
+ curved_<f64c_k>("bilinear_f64c_blas", bilinear_f64c_blas, simsimd_bilinear_f64c_serial);
779
+ curved_<f32_k>("bilinear_f32_blas", bilinear_f32_blas, simsimd_bilinear_f32_accurate);
780
+ curved_<f32c_k>("bilinear_f32c_blas", bilinear_f32c_blas, simsimd_bilinear_f32c_accurate);
781
+
733
782
  #endif
734
783
 
735
784
  #if SIMSIMD_TARGET_NEON
@@ -762,6 +811,7 @@ int main(int argc, char **argv) {
762
811
 
763
812
  curved_<f32_k>("bilinear_f32_neon", simsimd_bilinear_f32_neon, simsimd_bilinear_f32_accurate);
764
813
  curved_<f32_k>("mahalanobis_f32_neon", simsimd_mahalanobis_f32_neon, simsimd_mahalanobis_f32_accurate);
814
+ curved_<f32c_k>("bilinear_f32c_neon", simsimd_bilinear_f32c_neon, simsimd_bilinear_f32c_accurate);
765
815
 
766
816
  sparse_<u16_k>("intersect_u16_neon", simsimd_intersect_u16_neon, simsimd_intersect_u16_accurate);
767
817
  sparse_<u32_k>("intersect_u32_neon", simsimd_intersect_u32_neon, simsimd_intersect_u32_accurate);
@@ -786,6 +836,7 @@ int main(int argc, char **argv) {
786
836
 
787
837
  curved_<f16_k>("bilinear_f16_neon", simsimd_bilinear_f16_neon, simsimd_bilinear_f16_accurate);
788
838
  curved_<f16_k>("mahalanobis_f16_neon", simsimd_mahalanobis_f16_neon, simsimd_mahalanobis_f16_accurate);
839
+ curved_<f16c_k>("bilinear_f16c_neon", simsimd_bilinear_f16c_neon, simsimd_bilinear_f16c_accurate);
789
840
 
790
841
  fma_<f16_k>("fma_f16_neon", simsimd_fma_f16_neon, simsimd_fma_f16_accurate, simsimd_l2_f16_accurate);
791
842
  fma_<f16_k>("wsum_f16_neon", simsimd_wsum_f16_neon, simsimd_wsum_f16_accurate, simsimd_l2_f16_accurate);
@@ -808,17 +859,13 @@ int main(int argc, char **argv) {
808
859
 
809
860
  curved_<bf16_k>("bilinear_bf16_neon", simsimd_bilinear_bf16_neon, simsimd_bilinear_bf16_accurate);
810
861
  curved_<bf16_k>("mahalanobis_bf16_neon", simsimd_mahalanobis_bf16_neon, simsimd_mahalanobis_bf16_accurate);
862
+ curved_<bf16c_k>("bilinear_bf16c_neon", simsimd_bilinear_bf16c_neon, simsimd_bilinear_bf16c_accurate);
811
863
 
812
864
  fma_<bf16_k>("fma_bf16_neon", simsimd_fma_bf16_neon, simsimd_fma_bf16_accurate, simsimd_l2_bf16_accurate);
813
865
  fma_<bf16_k>("wsum_bf16_neon", simsimd_wsum_bf16_neon, simsimd_wsum_bf16_accurate, simsimd_l2_bf16_accurate);
814
866
  #endif
815
867
 
816
868
  #if SIMSIMD_TARGET_SVE
817
- dense_<f16_k>("dot_f16_sve", simsimd_dot_f16_sve, simsimd_dot_f16_accurate);
818
- dense_<f16_k>("cos_f16_sve", simsimd_cos_f16_sve, simsimd_cos_f16_accurate);
819
- dense_<f16_k>("l2sq_f16_sve", simsimd_l2sq_f16_sve, simsimd_l2sq_f16_accurate);
820
- dense_<f16_k>("l2_f16_sve", simsimd_l2_f16_sve, simsimd_l2_f16_accurate);
821
-
822
869
  dense_<f32_k>("dot_f32_sve", simsimd_dot_f32_sve, simsimd_dot_f32_accurate);
823
870
  dense_<f32_k>("cos_f32_sve", simsimd_cos_f32_sve, simsimd_cos_f32_accurate);
824
871
  dense_<f32_k>("l2sq_f32_sve", simsimd_l2sq_f32_sve, simsimd_l2sq_f32_accurate);
@@ -914,12 +961,12 @@ int main(int argc, char **argv) {
914
961
  dense_<bf16_k>("cos_bf16_genoa", simsimd_cos_bf16_genoa, simsimd_cos_bf16_accurate);
915
962
  dense_<bf16_k>("l2sq_bf16_genoa", simsimd_l2sq_bf16_genoa, simsimd_l2sq_bf16_accurate);
916
963
  dense_<bf16_k>("l2_bf16_genoa", simsimd_l2_bf16_genoa, simsimd_l2_bf16_accurate);
917
-
918
- dense_<bf16_k>("dot_bf16c_genoa", simsimd_dot_bf16c_genoa, simsimd_dot_bf16c_accurate);
919
- dense_<bf16_k>("vdot_bf16c_genoa", simsimd_vdot_bf16c_genoa, simsimd_vdot_bf16c_accurate);
964
+ dense_<bf16c_k>("dot_bf16c_genoa", simsimd_dot_bf16c_genoa, simsimd_dot_bf16c_accurate);
965
+ dense_<bf16c_k>("vdot_bf16c_genoa", simsimd_vdot_bf16c_genoa, simsimd_vdot_bf16c_accurate);
920
966
 
921
967
  curved_<bf16_k>("bilinear_bf16_genoa", simsimd_bilinear_bf16_genoa, simsimd_bilinear_bf16_accurate);
922
968
  curved_<bf16_k>("mahalanobis_bf16_genoa", simsimd_mahalanobis_bf16_genoa, simsimd_mahalanobis_bf16_accurate);
969
+ curved_<bf16c_k>("bilinear_bf16c_genoa", simsimd_bilinear_bf16c_genoa, simsimd_bilinear_bf16c_accurate);
923
970
  #endif
924
971
 
925
972
  #if SIMSIMD_TARGET_SAPPHIRE
@@ -937,6 +984,10 @@ int main(int argc, char **argv) {
937
984
  fma_<u8_k>("wsum_u8_sapphire", simsimd_wsum_u8_sapphire, simsimd_wsum_u8_accurate, simsimd_l2_u8_serial);
938
985
  fma_<i8_k>("fma_i8_sapphire", simsimd_fma_i8_sapphire, simsimd_fma_i8_accurate, simsimd_l2_i8_serial);
939
986
  fma_<i8_k>("wsum_i8_sapphire", simsimd_wsum_i8_sapphire, simsimd_wsum_i8_accurate, simsimd_l2_i8_serial);
987
+
988
+ curved_<f16_k>("bilinear_f16_sapphire", simsimd_bilinear_f16_sapphire, simsimd_bilinear_f16_accurate);
989
+ curved_<f16_k>("mahalanobis_f16_sapphire", simsimd_mahalanobis_f16_sapphire, simsimd_mahalanobis_f16_accurate);
990
+ curved_<f16c_k>("bilinear_f16c_sapphire", simsimd_bilinear_f16c_sapphire, simsimd_bilinear_f16c_accurate);
940
991
  #endif
941
992
 
942
993
  #if SIMSIMD_TARGET_ICE
@@ -987,6 +1038,10 @@ int main(int argc, char **argv) {
987
1038
  fma_<bf16_k>("fma_bf16_skylake", simsimd_fma_bf16_skylake, simsimd_fma_bf16_accurate, simsimd_l2_bf16_accurate);
988
1039
  fma_<bf16_k>("wsum_bf16_skylake", simsimd_wsum_bf16_skylake, simsimd_wsum_bf16_accurate, simsimd_l2_bf16_accurate);
989
1040
 
1041
+ curved_<f32_k>("bilinear_f32_skylake", simsimd_bilinear_f32_skylake, simsimd_bilinear_f32_serial);
1042
+ curved_<f32c_k>("bilinear_f32c_skylake", simsimd_bilinear_f32c_skylake, simsimd_bilinear_f32c_serial);
1043
+ curved_<f64_k>("bilinear_f64_skylake", simsimd_bilinear_f64_skylake, simsimd_bilinear_f64_serial);
1044
+ curved_<f64c_k>("bilinear_f64c_skylake", simsimd_bilinear_f64c_skylake, simsimd_bilinear_f64c_serial);
990
1045
  #endif
991
1046
 
992
1047
  sparse_<u16_k>("intersect_u16_serial", simsimd_intersect_u16_serial, simsimd_intersect_u16_accurate);
@@ -995,12 +1050,16 @@ int main(int argc, char **argv) {
995
1050
  sparse_<u32_k>("intersect_u32_accurate", simsimd_intersect_u32_accurate, simsimd_intersect_u32_accurate);
996
1051
 
997
1052
  curved_<f64_k>("bilinear_f64_serial", simsimd_bilinear_f64_serial, simsimd_bilinear_f64_serial);
1053
+ curved_<f64c_k>("bilinear_f64c_serial", simsimd_bilinear_f64c_serial, simsimd_bilinear_f64c_serial);
998
1054
  curved_<f64_k>("mahalanobis_f64_serial", simsimd_mahalanobis_f64_serial, simsimd_mahalanobis_f64_serial);
999
1055
  curved_<f32_k>("bilinear_f32_serial", simsimd_bilinear_f32_serial, simsimd_bilinear_f32_accurate);
1056
+ curved_<f32c_k>("bilinear_f32c_serial", simsimd_bilinear_f32c_serial, simsimd_bilinear_f32c_accurate);
1000
1057
  curved_<f32_k>("mahalanobis_f32_serial", simsimd_mahalanobis_f32_serial, simsimd_mahalanobis_f32_accurate);
1001
1058
  curved_<f16_k>("bilinear_f16_serial", simsimd_bilinear_f16_serial, simsimd_bilinear_f16_accurate);
1059
+ curved_<f16c_k>("bilinear_f16c_serial", simsimd_bilinear_f16c_serial, simsimd_bilinear_f16c_accurate);
1002
1060
  curved_<f16_k>("mahalanobis_f16_serial", simsimd_mahalanobis_f16_serial, simsimd_mahalanobis_f16_accurate);
1003
1061
  curved_<bf16_k>("bilinear_bf16_serial", simsimd_bilinear_bf16_serial, simsimd_bilinear_bf16_accurate);
1062
+ curved_<bf16c_k>("bilinear_bf16c_serial", simsimd_bilinear_bf16c_serial, simsimd_bilinear_bf16c_accurate);
1004
1063
  curved_<bf16_k>("mahalanobis_bf16_serial", simsimd_mahalanobis_bf16_serial, simsimd_mahalanobis_bf16_accurate);
1005
1064
 
1006
1065
  dense_<bf16_k>("dot_bf16_serial", simsimd_dot_bf16_serial, simsimd_dot_bf16_accurate);
@@ -90,6 +90,12 @@ void test_distance_from_itself(void) {
90
90
  simsimd_f32_t f32s[1536];
91
91
  simsimd_f16_t f16s[1536];
92
92
  simsimd_bf16_t bf16s[1536];
93
+
94
+ simsimd_f64c_t f64cs[768];
95
+ simsimd_f32c_t f32cs[768];
96
+ simsimd_f16c_t f16cs[768];
97
+ simsimd_bf16c_t bf16cs[768];
98
+
93
99
  simsimd_i8_t i8s[1536];
94
100
  simsimd_u8_t u8s[1536];
95
101
  simsimd_b8_t b8s[1536 / 8]; // 8 bits per word
@@ -120,16 +126,16 @@ void test_distance_from_itself(void) {
120
126
  simsimd_dot_f64(f64s, f64s, 1536, &distance[0]);
121
127
 
122
128
  // Complex inner product between two vectors
123
- simsimd_dot_f16c(f16s, f16s, 1536, &distance[0]);
124
- simsimd_dot_bf16c(bf16s, bf16s, 1536, &distance[0]);
125
- simsimd_dot_f32c(f32s, f32s, 1536, &distance[0]);
126
- simsimd_dot_f64c(f64s, f64s, 1536, &distance[0]);
129
+ simsimd_dot_bf16c(bf16cs, bf16cs, 768, &distance[0]);
130
+ simsimd_dot_f16c(f16cs, f16cs, 768, &distance[0]);
131
+ simsimd_dot_f32c(f32cs, f32cs, 768, &distance[0]);
132
+ simsimd_dot_f64c(f64cs, f64cs, 768, &distance[0]);
127
133
 
128
134
  // Complex conjugate inner product between two vectors
129
- simsimd_vdot_f16c(f16s, f16s, 1536, &distance[0]);
130
- simsimd_vdot_bf16c(bf16s, bf16s, 1536, &distance[0]);
131
- simsimd_vdot_f32c(f32s, f32s, 1536, &distance[0]);
132
- simsimd_vdot_f64c(f64s, f64s, 1536, &distance[0]);
135
+ simsimd_vdot_bf16c(bf16cs, bf16cs, 768, &distance[0]);
136
+ simsimd_vdot_f16c(f16cs, f16cs, 768, &distance[0]);
137
+ simsimd_vdot_f32c(f32cs, f32cs, 768, &distance[0]);
138
+ simsimd_vdot_f64c(f64cs, f64cs, 768, &distance[0]);
133
139
 
134
140
  // Hamming distance between two vectors
135
141
  simsimd_hamming_b8(b8s, b8s, 1536 / 8, &distance[0]);