usearch 2.17.1 → 2.19.5

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 (47) hide show
  1. package/README.md +6 -2
  2. package/c/lib.cpp +13 -2
  3. package/c/module.modulemap +4 -0
  4. package/c/usearch.h +13 -4
  5. package/cpp/README.md +1 -1
  6. package/cpp/bench.cpp +2 -2
  7. package/cpp/test.cpp +110 -16
  8. package/include/usearch/index.hpp +91 -35
  9. package/include/usearch/index_dense.hpp +34 -19
  10. package/include/usearch/index_plugins.hpp +29 -7
  11. package/javascript/README.md +8 -6
  12. package/javascript/dist/cjs/usearch.d.ts +9 -4
  13. package/javascript/dist/cjs/usearch.js +23 -8
  14. package/javascript/dist/esm/usearch.d.ts +9 -4
  15. package/javascript/dist/esm/usearch.js +21 -7
  16. package/javascript/lib.cpp +137 -59
  17. package/javascript/usearch.test.js +359 -192
  18. package/javascript/usearch.ts +35 -11
  19. package/package.json +1 -1
  20. package/prebuilds/darwin-arm64+x64/usearch.node +0 -0
  21. package/prebuilds/linux-arm64/usearch.node +0 -0
  22. package/prebuilds/linux-x64/usearch.node +0 -0
  23. package/simsimd/CONTRIBUTING.md +23 -8
  24. package/simsimd/Cargo.lock +1 -1
  25. package/simsimd/Cargo.toml +1 -1
  26. package/simsimd/README.md +49 -9
  27. package/simsimd/build.rs +7 -3
  28. package/simsimd/c/lib.c +6 -1
  29. package/simsimd/include/simsimd/binary.h +3 -3
  30. package/simsimd/include/simsimd/dot.h +2 -2
  31. package/simsimd/include/simsimd/elementwise.h +26 -17
  32. package/simsimd/include/simsimd/simsimd.h +125 -49
  33. package/simsimd/include/simsimd/sparse.h +30 -29
  34. package/simsimd/include/simsimd/spatial.h +4 -3
  35. package/simsimd/include/simsimd/types.h +19 -8
  36. package/simsimd/javascript/lib.c +0 -1
  37. package/simsimd/package-lock.json +411 -362
  38. package/simsimd/package.json +9 -9
  39. package/simsimd/rust/lib.rs +63 -29
  40. package/simsimd/scripts/bench.cxx +13 -0
  41. package/simsimd/scripts/bench_cosine.rs +8 -0
  42. package/simsimd/scripts/bench_native.rs +4 -0
  43. package/simsimd/scripts/bench_sqeuclidean.rs +8 -0
  44. package/simsimd/scripts/test.c +22 -0
  45. package/simsimd/scripts/test.py +80 -4
  46. package/simsimd/swift/SimSIMD.swift +17 -0
  47. package/simsimd/swift/Test.swift +36 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "simsimd",
3
- "version": "6.2.3",
3
+ "version": "6.4.10",
4
4
  "description": "Portable mixed-precision BLAS-like vector math library for x86 and ARM",
5
5
  "homepage": "https://github.com/ashvardanian/simsimd",
6
6
  "author": "Ash Vardanian",
@@ -43,19 +43,19 @@
43
43
  },
44
44
  "dependencies": {
45
45
  "bindings": "^1.5.0",
46
- "node-addon-api": "^3.2.1",
47
- "node-gyp-build": "^4.8.0"
46
+ "node-addon-api": "^8.4.0",
47
+ "node-gyp-build": "^4.8.4"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@types/bindings": "^1.5.5",
51
- "@types/node": "^20.17.6",
52
- "node-gyp": "^10.0.1",
53
- "prebuildify": "^6.0.0",
54
- "typescript": "^5.3.3"
51
+ "@types/node": "^24.0.10",
52
+ "node-gyp": "^11.2.0",
53
+ "prebuildify": "^6.0.1",
54
+ "typescript": "^5.8.3"
55
55
  },
56
56
  "optionalDependencies": {
57
57
  "benchmark": "^2.1.4",
58
- "mathjs": "^11.11.2",
59
- "usearch": "^2.8.15"
58
+ "mathjs": "^14.5.3",
59
+ "usearch": "^2.17.1"
60
60
  }
61
61
  }
@@ -2,7 +2,7 @@
2
2
  //!
3
3
  //! * Targets ARM NEON, SVE, x86 AVX2, AVX-512 (VNNI, FP16) hardware backends.
4
4
  //! * Handles `f64` double-, `f32` single-, and `f16` half-precision, `i8` integral, and binary vectors.
5
- //! * Zero-dependency header-only C 99 library with bindings for Rust and other langauges.
5
+ //! * Zero-dependency header-only C 99 library with bindings for Rust and other languages.
6
6
  //!
7
7
  //! ## Implemented distance functions include:
8
8
  //!
@@ -26,6 +26,9 @@
26
26
  //!
27
27
  //! // Compute squared Euclidean distance
28
28
  //! let l2sq_dist = i8::l2sq(a, b);
29
+ //!
30
+ //! // Optimize performance by flushing denormals
31
+ //! simsimd::capabilities::flush_denormals();
29
32
  //! ```
30
33
  //!
31
34
  //! ## Traits
@@ -53,6 +56,7 @@ type ComplexProduct = (f64, f64);
53
56
 
54
57
  extern "C" {
55
58
 
59
+ fn simsimd_dot_i8(a: *const i8, b: *const i8, c: usize, d: *mut Distance);
56
60
  fn simsimd_dot_f16(a: *const u16, b: *const u16, c: usize, d: *mut Distance);
57
61
  fn simsimd_dot_bf16(a: *const u16, b: *const u16, c: usize, d: *mut Distance);
58
62
  fn simsimd_dot_f32(a: *const f32, b: *const f32, c: usize, d: *mut Distance);
@@ -99,8 +103,20 @@ extern "C" {
99
103
  fn simsimd_kl_f32(a: *const f32, b: *const f32, c: usize, d: *mut Distance);
100
104
  fn simsimd_kl_f64(a: *const f64, b: *const f64, c: usize, d: *mut Distance);
101
105
 
102
- fn simsimd_intersect_u16(a: *const u16, b: *const u16, a_length: usize, b_length: usize, d: *mut Distance);
103
- fn simsimd_intersect_u32(a: *const u32, b: *const u32, a_length: usize, b_length: usize, d: *mut Distance);
106
+ fn simsimd_intersect_u16(
107
+ a: *const u16,
108
+ b: *const u16,
109
+ a_length: usize,
110
+ b_length: usize,
111
+ d: *mut Distance,
112
+ );
113
+ fn simsimd_intersect_u32(
114
+ a: *const u32,
115
+ b: *const u32,
116
+ a_length: usize,
117
+ b_length: usize,
118
+ d: *mut Distance,
119
+ );
104
120
 
105
121
  fn simsimd_uses_neon() -> i32;
106
122
  fn simsimd_uses_neon_f16() -> i32;
@@ -117,6 +133,9 @@ extern "C" {
117
133
  fn simsimd_uses_sapphire() -> i32;
118
134
  fn simsimd_uses_turin() -> i32;
119
135
  fn simsimd_uses_sierra() -> i32;
136
+
137
+ fn simsimd_flush_denormals() -> i32;
138
+ fn simsimd_uses_dynamic_dispatch() -> i32;
120
139
  }
121
140
 
122
141
  /// A half-precision floating point number.
@@ -194,6 +213,29 @@ pub mod capabilities {
194
213
  pub fn uses_sierra() -> bool {
195
214
  unsafe { crate::simsimd_uses_sierra() != 0 }
196
215
  }
216
+
217
+ /// Flushes denormalized numbers to zero on the current CPU architecture.
218
+ ///
219
+ /// This function should be called on each thread before any SIMD operations
220
+ /// to avoid performance penalties. When facing denormalized values,
221
+ /// Fused-Multiply-Add (FMA) operations can be up to 30x slower.
222
+ ///
223
+ /// # Returns
224
+ ///
225
+ /// Returns `true` if the operation was successful, `false` otherwise.
226
+ pub fn flush_denormals() -> bool {
227
+ unsafe { crate::simsimd_flush_denormals() != 0 }
228
+ }
229
+
230
+ /// Checks if the library is using dynamic dispatch for function selection.
231
+ ///
232
+ /// # Returns
233
+ ///
234
+ /// Returns `true` if dynamic dispatch is enabled, `false` otherwise.
235
+ /// Currently always returns `false` as dynamic dispatch is not implemented.
236
+ pub fn uses_dynamic_dispatch() -> bool {
237
+ unsafe { crate::simsimd_uses_dynamic_dispatch() != 0 }
238
+ }
197
239
  }
198
240
 
199
241
  /// `SpatialSimilarity` provides a set of trait methods for computing similarity
@@ -225,8 +267,8 @@ where
225
267
  fn l2sq(a: &[Self], b: &[Self]) -> Option<Distance>;
226
268
 
227
269
  /// Computes the Euclidean distance between two slices.
228
- /// The Euclidean distance is the square root of
229
- // sum of the squared differences between corresponding
270
+ /// The Euclidean distance is the square root of
271
+ // sum of the squared differences between corresponding
230
272
  /// elements of the two slices.
231
273
  fn l2(a: &[Self], b: &[Self]) -> Option<Distance>;
232
274
 
@@ -238,8 +280,8 @@ where
238
280
  }
239
281
 
240
282
  /// Computes the Euclidean distance between two slices.
241
- /// The Euclidean distance is the square root of the
242
- /// sum of the squared differences between corresponding
283
+ /// The Euclidean distance is the square root of the
284
+ /// sum of the squared differences between corresponding
243
285
  /// elements of the two slices.
244
286
  fn euclidean(a: &[Self], b: &[Self]) -> Option<Distance> {
245
287
  SpatialSimilarity::l2(a, b)
@@ -367,7 +409,7 @@ impl SpatialSimilarity for i8 {
367
409
  }
368
410
  let mut distance_value: Distance = 0.0;
369
411
  let distance_ptr: *mut Distance = &mut distance_value as *mut Distance;
370
- unsafe { simsimd_cos_i8(a.as_ptr(), b.as_ptr(), a.len(), distance_ptr) };
412
+ unsafe { simsimd_dot_i8(a.as_ptr(), b.as_ptr(), a.len(), distance_ptr) };
371
413
  Some(distance_value)
372
414
  }
373
415
 
@@ -393,25 +435,21 @@ impl SpatialSimilarity for i8 {
393
435
  }
394
436
 
395
437
  impl Sparse for u16 {
396
-
397
438
  fn intersect(a: &[Self], b: &[Self]) -> Option<Distance> {
398
439
  let mut distance_value: Distance = 0.0;
399
440
  let distance_ptr: *mut Distance = &mut distance_value as *mut Distance;
400
441
  unsafe { simsimd_intersect_u16(a.as_ptr(), b.as_ptr(), a.len(), b.len(), distance_ptr) };
401
442
  Some(distance_value)
402
443
  }
403
-
404
444
  }
405
445
 
406
446
  impl Sparse for u32 {
407
-
408
447
  fn intersect(a: &[Self], b: &[Self]) -> Option<Distance> {
409
448
  let mut distance_value: Distance = 0.0;
410
449
  let distance_ptr: *mut Distance = &mut distance_value as *mut Distance;
411
450
  unsafe { simsimd_intersect_u32(a.as_ptr(), b.as_ptr(), a.len(), b.len(), distance_ptr) };
412
451
  Some(distance_value)
413
452
  }
414
-
415
453
  }
416
454
 
417
455
  impl SpatialSimilarity for f16 {
@@ -458,7 +496,6 @@ impl SpatialSimilarity for f16 {
458
496
  }
459
497
 
460
498
  fn l2(a: &[Self], b: &[Self]) -> Option<Distance> {
461
-
462
499
  if a.len() != b.len() {
463
500
  return None;
464
501
  }
@@ -524,7 +561,7 @@ impl SpatialSimilarity for bf16 {
524
561
  let b_ptr = b.as_ptr() as *const u16;
525
562
  let mut distance_value: Distance = 0.0;
526
563
  let distance_ptr: *mut Distance = &mut distance_value as *mut Distance;
527
- unsafe { simsimd_l2sq_bf16(a_ptr, b_ptr, a.len(), distance_ptr) };
564
+ unsafe { simsimd_l2_bf16(a_ptr, b_ptr, a.len(), distance_ptr) };
528
565
  Some(distance_value)
529
566
  }
530
567
  }
@@ -891,7 +928,7 @@ mod tests {
891
928
 
892
929
  if let Some(result) = SpatialSimilarity::dot(a, b) {
893
930
  println!("The result of dot_i8 is {:.8}", result);
894
- assert_almost_equal(0.029403687, result, 0.01);
931
+ assert_almost_equal(32.0, result, 0.01);
895
932
  }
896
933
  }
897
934
 
@@ -993,7 +1030,6 @@ mod tests {
993
1030
  .map(|&x| HalfF16::from_f32(x))
994
1031
  .collect();
995
1032
 
996
-
997
1033
  let a_simsimd: &[f16] =
998
1034
  unsafe { std::slice::from_raw_parts(a_half.as_ptr() as *const f16, a_half.len()) };
999
1035
  let b_simsimd: &[f16] =
@@ -1003,7 +1039,6 @@ mod tests {
1003
1039
  println!("The result of l2_f16 is {:.8}", result);
1004
1040
  assert_almost_equal(5.2, result, 0.01);
1005
1041
  }
1006
-
1007
1042
  }
1008
1043
 
1009
1044
  #[test]
@@ -1160,8 +1195,8 @@ mod tests {
1160
1195
  #[test]
1161
1196
  fn test_intersect_u16() {
1162
1197
  {
1163
- let a_u16: &[u16] = &[153, 16384, 17408];
1164
- let b_u16: &[u16] = &[15360, 16384, 7408];
1198
+ let a_u16: &[u16] = &[153, 16384, 17408];
1199
+ let b_u16: &[u16] = &[15360, 16384, 7408];
1165
1200
 
1166
1201
  if let Some(result) = Sparse::intersect(a_u16, b_u16) {
1167
1202
  println!("The result of intersect_u16 is {:.8}", result);
@@ -1170,37 +1205,36 @@ mod tests {
1170
1205
  }
1171
1206
 
1172
1207
  {
1173
- let a_u16: &[u16] = &[153, 11638, 08];
1174
- let b_u16: &[u16] = &[15360, 16384, 7408];
1208
+ let a_u16: &[u16] = &[153, 11638, 08];
1209
+ let b_u16: &[u16] = &[15360, 16384, 7408];
1175
1210
 
1176
1211
  if let Some(result) = Sparse::intersect(a_u16, b_u16) {
1177
1212
  println!("The result of intersect_u16 is {:.8}", result);
1178
1213
  assert_almost_equal(0.0, result, 0.0001);
1179
- }
1214
+ }
1180
1215
  }
1181
1216
  }
1182
1217
 
1183
1218
  #[test]
1184
1219
  fn test_intersect_u32() {
1185
1220
  {
1186
- let a_u32: &[u32] = &[11, 153];
1187
- let b_u32: &[u32] = &[11, 153, 7408, 16384];
1221
+ let a_u32: &[u32] = &[11, 153];
1222
+ let b_u32: &[u32] = &[11, 153, 7408, 16384];
1188
1223
 
1189
1224
  if let Some(result) = Sparse::intersect(a_u32, b_u32) {
1190
1225
  println!("The result of intersect_u32 is {:.8}", result);
1191
1226
  assert_almost_equal(2.0, result, 0.0001);
1192
1227
  }
1193
1228
  }
1194
-
1229
+
1195
1230
  {
1196
- let a_u32: &[u32] = &[153, 7408, 11638];
1197
- let b_u32: &[u32] = &[153, 7408, 11638];
1231
+ let a_u32: &[u32] = &[153, 7408, 11638];
1232
+ let b_u32: &[u32] = &[153, 7408, 11638];
1198
1233
 
1199
1234
  if let Some(result) = Sparse::intersect(a_u32, b_u32) {
1200
1235
  println!("The result of intersect_u32 is {:.8}", result);
1201
1236
  assert_almost_equal(3.0, result, 0.0001);
1202
- }
1237
+ }
1203
1238
  }
1204
-
1205
1239
  }
1206
1240
  }
@@ -1,3 +1,16 @@
1
+ /**
2
+ * SimSIMD C++ Benchmark Suite
3
+ *
4
+ * Comprehensive benchmarks comparing SimSIMD SIMD-optimized functions against
5
+ * baseline implementations using Google Benchmark framework. Run with:
6
+ *
7
+ * ```bash
8
+ * cmake -B build_release -D SIMSIMD_BUILD_BENCHMARKS=1
9
+ * cmake --build build_release
10
+ * build_release/simsimd_bench
11
+ * ```
12
+ */
13
+
1
14
  #include <array> // `std::array`
2
15
  #include <cmath> // `std::sqrt`
3
16
  #include <cstdlib> // `std::aligned_alloc`
@@ -1,3 +1,11 @@
1
+ //! Cosine Similarity Benchmark
2
+ //!
3
+ //! Compares SimSIMD vs native Rust implementations using Criterion.
4
+ //! Run with:
5
+ //!
6
+ //! ```bash
7
+ //! cargo bench --bench bench_cosine
8
+ //! ```
1
9
  use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
2
10
  use simsimd::SpatialSimilarity as SimSIMD;
3
11
 
@@ -1,3 +1,7 @@
1
+ //! Native Rust Baseline Implementations
2
+ //!
3
+ //! Provides baseline implementations of distance functions for benchmarking
4
+ //! against SimSIMD's SIMD-optimized versions.
1
5
  #![allow(unused)]
2
6
  use rand::Rng;
3
7
 
@@ -1,3 +1,11 @@
1
+ //! Squared Euclidean Distance Benchmark
2
+ //!
3
+ //! Compares SimSIMD vs native Rust implementations using Criterion.
4
+ //! Run with:
5
+ //!
6
+ //! ```bash
7
+ //! cargo bench --bench bench_sqeuclidean
8
+ //! ```
1
9
  use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
2
10
  use simsimd::SpatialSimilarity as SimSIMD;
3
11
 
@@ -156,10 +156,32 @@ void test_distance_from_itself(void) {
156
156
  simsimd_kl_f64(f64s, f64s, 1536, &distance[0]);
157
157
  }
158
158
 
159
+ /**
160
+ * @brief Test whether denormals are being flushed to zero or not.
161
+ *
162
+ * We create subnormal float values, do a small computation (multiplication),
163
+ * and classify the result. If flush-to-zero @b (FTZ) is enabled, the result is
164
+ * likely zero. Otherwise, you may see another subnormal or normal number.
165
+ */
166
+ static void test_denormals(void) {
167
+
168
+ // Create two subnormal floats:
169
+ // 1e-40 ~ 1.0 * 10^-40 is typically a subnormal in IEEE-754 single precision
170
+ float subnorm1 = 1e-40f;
171
+ float subnorm2 = 2e-40f;
172
+ float result = subnorm1 * subnorm2; // This might be subnormal, zero, or normal
173
+ int classification = fpclassify(result);
174
+ if (classification == FP_SUBNORMAL) { printf("Denormal test: result is subnormal: %.8g\n", result); }
175
+ else if (result == 0.0f) { printf("Denormal test: result is zero (denormals likely flushed).\n"); }
176
+ else if (classification == FP_NORMAL) { printf("Denormal test: result is normal: %.8g\n", result); }
177
+ else { printf("Denormal test: result has unexpected classification.\n"); }
178
+ }
179
+
159
180
  int main(int argc, char **argv) {
160
181
 
161
182
  print_capabilities();
162
183
  test_utilities();
163
184
  test_distance_from_itself();
185
+ test_denormals();
164
186
  return 0;
165
187
  }
@@ -4,8 +4,8 @@
4
4
  Module: test.py
5
5
 
6
6
  This module contains a suite of tests for the `simsimd` package.
7
- It compares various SIMD kernels (like Dot-products, squared Euclidean, and Cosine distances)
8
- with their NumPy or baseline counterparts, testing accuracy for different data types including
7
+ It compares various SIMD kernels (like Dot-products, squared Euclidean, and Cosine distances)
8
+ with their NumPy or baseline counterparts, testing accuracy for different data types including
9
9
  floating-point, integer, and complex numbers.
10
10
 
11
11
  The tests cover:
@@ -1245,7 +1245,7 @@ def test_batch(ndim, dtype, capability):
1245
1245
  result_simd = np.array(simd.sqeuclidean(A, B)).astype(np.float64)
1246
1246
  assert np.allclose(result_simd, result_np, atol=SIMSIMD_ATOL, rtol=SIMSIMD_RTOL)
1247
1247
 
1248
- # Distance between matrixes A (N x D scalars) and B (N x D scalars) with a differnt output type.
1248
+ # Distance between matrixes A (N x D scalars) and B (N x D scalars) with a different output type.
1249
1249
  A = np.random.randn(10, ndim).astype(dtype)
1250
1250
  B = np.random.randn(10, ndim).astype(dtype)
1251
1251
  result_np = np.array([spd.sqeuclidean(A[i], B[i]) for i in range(10)]).astype(np.float32)
@@ -1365,7 +1365,7 @@ def test_cdist_complex(ndim, input_dtype, out_dtype, metric, capability):
1365
1365
  C = C_extended[:, :N]
1366
1366
 
1367
1367
  #! Unlike the `np.dot`, the `np.vdot` flattens multi-dimensional inputs into 1D arrays.
1368
- #! So to compreare the results we need to manually compute all the dot-products.
1368
+ #! So to compare the results we need to manually compute all the dot-products.
1369
1369
  expected = np.zeros((M, N), dtype=out_dtype if out_dtype else np.complex128)
1370
1370
  baseline_kernel = np.dot if metric == "dot" else np.vdot
1371
1371
  for i in range(M):
@@ -1418,6 +1418,82 @@ def test_cdist_hamming(ndim, out_dtype, capability):
1418
1418
  np.testing.assert_allclose(result, expected, atol=SIMSIMD_ATOL, rtol=SIMSIMD_RTOL)
1419
1419
 
1420
1420
 
1421
+ @pytest.mark.skipif(not numpy_available, reason="NumPy is not installed")
1422
+ def test_gil_free_threading():
1423
+ """Test SimSIMD in Python 3.13t free-threaded mode if available."""
1424
+ import sys
1425
+ import sysconfig
1426
+
1427
+ # Check if we're in a GIL-free environment
1428
+ # https://py-free-threading.github.io/running-gil-disabled/
1429
+ version = sys.version_info
1430
+ if version.major == 3 and version.minor >= 13:
1431
+ is_free_threaded = bool(sysconfig.get_config_var("Py_GIL_DISABLED"))
1432
+ if not is_free_threaded:
1433
+ pytest.skip("Uses non-free-threaded Python, skipping GIL-related tests")
1434
+ if sys._is_gil_enabled():
1435
+ pytest.skip("GIL is enabled, skipping GIL-related tests")
1436
+ else:
1437
+ pytest.skip("Python < 3.13t, skipping GIL-related tests")
1438
+
1439
+ import multiprocessing
1440
+ import concurrent.futures
1441
+
1442
+ num_threads = multiprocessing.cpu_count()
1443
+ vectors_a = np.random.rand(32 * 1024 * num_threads, 1024).astype(np.float32)
1444
+ vectors_b = np.random.rand(32 * 1024 * num_threads, 1024).astype(np.float32)
1445
+ distances = np.zeros(vectors_a.shape[0], dtype=np.float32)
1446
+
1447
+ def compute_batch(start_idx, end_idx) -> float:
1448
+ """Compute cosine distances for a batch."""
1449
+ slice_a = vectors_a[start_idx:end_idx]
1450
+ slice_b = vectors_b[start_idx:end_idx]
1451
+ slice_distances = distances[start_idx:end_idx]
1452
+ simd.cosine(slice_a, slice_b, out=slice_distances)
1453
+ return sum(slice_distances)
1454
+
1455
+ def compute_with_threads(threads: int) -> float:
1456
+ """Compute cosine distances using multiple threads."""
1457
+ chunk_size = len(vectors_a) // threads
1458
+ futures = []
1459
+ with concurrent.futures.ThreadPoolExecutor(max_workers=threads) as executor:
1460
+ for i in range(threads):
1461
+ start_idx = i * chunk_size
1462
+ end_idx = (i + 1) * chunk_size if i < threads - 1 else len(vectors_a)
1463
+ futures.append(executor.submit(compute_batch, start_idx, end_idx))
1464
+
1465
+ total_sum = 0.0
1466
+ for future in concurrent.futures.as_completed(futures):
1467
+ total_sum += future.result()
1468
+
1469
+ return total_sum
1470
+
1471
+ # Dual-threaded baseline is better than single-threaded,
1472
+ # as it will include the overhead of thread management.
1473
+ start_time = time.time()
1474
+ baseline_sum = compute_with_threads(2)
1475
+ end_time = time.time()
1476
+ baseline_duration = end_time - start_time
1477
+
1478
+ # Multi-threaded execution, using all available threads
1479
+ start_time = time.time()
1480
+ multi_sum = compute_with_threads(num_threads)
1481
+ end_time = time.time()
1482
+ multi_duration = end_time - start_time
1483
+
1484
+ # Verify results are the same length and reasonable
1485
+ assert np.allclose(
1486
+ baseline_sum, multi_sum, atol=SIMSIMD_ATOL, rtol=SIMSIMD_RTOL
1487
+ ), f"Results differ: baseline {baseline_sum} vs multi-threaded {multi_sum}"
1488
+
1489
+ # Warn if multi-threaded execution is slower than the baseline
1490
+ if baseline_duration < multi_duration:
1491
+ pytest.warns(
1492
+ UserWarning,
1493
+ f"{num_threads}-threaded execution took longer than 2-threaded baseline: {multi_duration:.2f}s vs {baseline_duration:.2f}s",
1494
+ )
1495
+
1496
+
1421
1497
  if __name__ == "__main__":
1422
1498
  pytest.main(
1423
1499
  [
@@ -4,6 +4,7 @@ public protocol SimSIMD {
4
4
  static var dataType: simsimd_datatype_t { get }
5
5
  static var cosine: simsimd_metric_dense_punned_t { get }
6
6
  static var dotProduct: simsimd_metric_dense_punned_t { get }
7
+ static var euclidean: simsimd_metric_dense_punned_t { get }
7
8
  static var squaredEuclidean: simsimd_metric_dense_punned_t { get }
8
9
  }
9
10
 
@@ -11,21 +12,26 @@ extension Int8: SimSIMD {
11
12
  public static let dataType = simsimd_datatype_i8_k
12
13
  public static let cosine = find(kind: simsimd_metric_cosine_k, dataType: dataType)
13
14
  public static let dotProduct = find(kind: simsimd_metric_dot_k, dataType: dataType)
15
+ public static let euclidean = find(kind: simsimd_metric_euclidean_k, dataType: dataType)
14
16
  public static let squaredEuclidean = find(kind: simsimd_metric_sqeuclidean_k, dataType: dataType)
15
17
  }
16
18
 
19
+ #if !arch(x86_64)
17
20
  @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *)
18
21
  extension Float16: SimSIMD {
19
22
  public static let dataType = simsimd_datatype_f16_k
20
23
  public static let cosine = find(kind: simsimd_metric_cosine_k, dataType: dataType)
21
24
  public static let dotProduct = find(kind: simsimd_metric_dot_k, dataType: dataType)
25
+ public static let euclidean = find(kind: simsimd_metric_euclidean_k, dataType: dataType)
22
26
  public static let squaredEuclidean = find(kind: simsimd_metric_sqeuclidean_k, dataType: dataType)
23
27
  }
28
+ #endif
24
29
 
25
30
  extension Float32: SimSIMD {
26
31
  public static let dataType = simsimd_datatype_f32_k
27
32
  public static let cosine = find(kind: simsimd_metric_cosine_k, dataType: dataType)
28
33
  public static let dotProduct = find(kind: simsimd_metric_inner_k, dataType: dataType)
34
+ public static let euclidean = find(kind: simsimd_metric_euclidean_k, dataType: dataType)
29
35
  public static let squaredEuclidean = find(kind: simsimd_metric_sqeuclidean_k, dataType: dataType)
30
36
  }
31
37
 
@@ -33,6 +39,7 @@ extension Float64: SimSIMD {
33
39
  public static let dataType = simsimd_datatype_f64_k
34
40
  public static let cosine = find(kind: simsimd_metric_cosine_k, dataType: dataType)
35
41
  public static let dotProduct = find(kind: simsimd_metric_dot_k, dataType: dataType)
42
+ public static let euclidean = find(kind: simsimd_metric_euclidean_k, dataType: dataType)
36
43
  public static let squaredEuclidean = find(kind: simsimd_metric_sqeuclidean_k, dataType: dataType)
37
44
  }
38
45
 
@@ -47,6 +54,11 @@ extension SimSIMD {
47
54
  perform(dotProduct, a: a, b: b)
48
55
  }
49
56
 
57
+ @inlinable @inline(__always)
58
+ public static func euclidean<A, B>(_ a: A, _ b: B) -> Double? where A: Sequence, B: Sequence, A.Element == Self, B.Element == Self {
59
+ perform(euclidean, a: a, b: b)
60
+ }
61
+
50
62
  @inlinable @inline(__always)
51
63
  public static func sqeuclidean<A, B>(_ a: A, _ b: B) -> Double? where A: Sequence, B: Sequence, A.Element == Self, B.Element == Self {
52
64
  perform(squaredEuclidean, a: a, b: b)
@@ -64,6 +76,11 @@ extension RandomAccessCollection where Element: SimSIMD {
64
76
  Element.dot(self, b)
65
77
  }
66
78
 
79
+ @inlinable @inline(__always)
80
+ public func euclidean<B>(_ b: B) -> Double? where B: Sequence, B.Element == Element {
81
+ Element.euclidean(self, b)
82
+ }
83
+
67
84
  @inlinable @inline(__always)
68
85
  public func sqeuclidean<B>(_ b: B) -> Double? where B: Sequence, B.Element == Element {
69
86
  Element.sqeuclidean(self, b)
@@ -13,12 +13,14 @@ class SimSIMDTests: XCTestCase {
13
13
  XCTAssertEqual(result, 0.00012027938, accuracy: 0.01)
14
14
  }
15
15
 
16
+ #if !arch(x86_64)
16
17
  func testCosineFloat16() throws {
17
18
  let a: [Float16] = [1.0, 2.0, 3.0]
18
19
  let b: [Float16] = [1.0, 2.0, 3.0]
19
20
  let result = try XCTUnwrap(a.cosine(b))
20
21
  XCTAssertEqual(result, 0.004930496, accuracy: 0.01)
21
22
  }
23
+ #endif
22
24
 
23
25
  func testCosineFloat32() throws {
24
26
  let a: [Float32] = [1.0, 2.0, 3.0]
@@ -41,12 +43,14 @@ class SimSIMDTests: XCTestCase {
41
43
  XCTAssertEqual(result, 32.0, accuracy: 0.01)
42
44
  }
43
45
 
46
+ #if !arch(x86_64)
44
47
  func testDotFloat16() throws {
45
48
  let a: [Float16] = [1.0, 2.0, 3.0]
46
49
  let b: [Float16] = [4.0, 5.0, 6.0]
47
50
  let result = try XCTUnwrap(a.dot(b))
48
51
  XCTAssertEqual(result, 32.0, accuracy: 0.01)
49
52
  }
53
+ #endif
50
54
 
51
55
  func testDotFloat32() throws {
52
56
  let a: [Float32] = [1.0, 2.0, 3.0]
@@ -62,6 +66,36 @@ class SimSIMDTests: XCTestCase {
62
66
  XCTAssertEqual(result, 32.0, accuracy: 0.01)
63
67
  }
64
68
 
69
+ func testEuclideanInt8() throws {
70
+ let a: [Int8] = [1, 2, 3]
71
+ let b: [Int8] = [4, 5, 6]
72
+ let result = try XCTUnwrap(a.euclidean(b))
73
+ XCTAssertEqual(result, 5.196152422706632, accuracy: 0.01)
74
+ }
75
+
76
+ #if !arch(x86_64)
77
+ func testEuclideanFloat16() throws {
78
+ let a: [Float16] = [1.0, 2.0, 3.0]
79
+ let b: [Float16] = [4.0, 5.0, 6.0]
80
+ let result = try XCTUnwrap(a.euclidean(b))
81
+ XCTAssertEqual(result, 5.196152422706632, accuracy: 0.01)
82
+ }
83
+ #endif
84
+
85
+ func testEuclideanFloat32() throws {
86
+ let a: [Float32] = [1.0, 2.0, 3.0]
87
+ let b: [Float32] = [4.0, 5.0, 6.0]
88
+ let result = try XCTUnwrap(a.euclidean(b))
89
+ XCTAssertEqual(result, 5.196152422706632, accuracy: 0.01)
90
+ }
91
+
92
+ func testEuclideanFloat64() throws {
93
+ let a: [Float64] = [1.0, 2.0, 3.0]
94
+ let b: [Float64] = [4.0, 5.0, 6.0]
95
+ let result = try XCTUnwrap(a.euclidean(b))
96
+ XCTAssertEqual(result, 5.196152422706632, accuracy: 0.01)
97
+ }
98
+
65
99
  func testSqeuclideanInt8() throws {
66
100
  let a: [Int8] = [1, 2, 3]
67
101
  let b: [Int8] = [4, 5, 6]
@@ -69,12 +103,14 @@ class SimSIMDTests: XCTestCase {
69
103
  XCTAssertEqual(result, 27.0, accuracy: 0.01)
70
104
  }
71
105
 
106
+ #if !arch(x86_64)
72
107
  func testSqeuclideanFloat16() throws {
73
108
  let a: [Float16] = [1.0, 2.0, 3.0]
74
109
  let b: [Float16] = [4.0, 5.0, 6.0]
75
110
  let result = try XCTUnwrap(a.sqeuclidean(b))
76
111
  XCTAssertEqual(result, 27.0, accuracy: 0.01)
77
112
  }
113
+ #endif
78
114
 
79
115
  func testSqeuclideanFloat32() throws {
80
116
  let a: [Float32] = [1.0, 2.0, 3.0]