usearch 2.21.2 → 2.21.4

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.
@@ -26,17 +26,19 @@
26
26
  *
27
27
  * Intel Palm Cove was an irrelevant intermediate release extending Skylake with IFMA and VBMI.
28
28
  * Intel Willow Cove was an irrelevant intermediate release extending Sunny Cove with VP2INTERSECT,
29
- * that aren't supported by any other CPU built to date... and those are only available in Tiger Lake laptops.
29
+ * which are not supported by other CPUs to date and are only available in Tiger Lake laptops.
30
30
  * Intel Cooper Lake was the only intermediary platform, that supported BF16, but not FP16.
31
31
  * It's mostly used in 4-socket and 8-socket high-memory configurations.
32
32
  *
33
- * In practical terms, it makes sense to differentiate only 3 AVX512 generations:
33
+ * For us, it makes sense to differentiate only these AVX512 generations:
34
34
  * 1. Intel Skylake (pre 2019): supports single-precision dot-products.
35
35
  * 2. Intel Ice Lake (2019-2021): advanced integer algorithms.
36
36
  * 3. AMD Genoa (2023+): brain-floating point support.
37
37
  * 4. Intel Sapphire Rapids (2023+): advanced mixed-precision float processing.
38
38
  * 5. AMD Turin (2024+): advanced sparse algorithms.
39
39
  *
40
+ * Beyond those, we support AVX2 for old Haswell generation CPUs, and AVX2+VNNI for modern Sierra generation.
41
+ *
40
42
  * To list all available macros for x86, take a recent compiler, like GCC 12 and run:
41
43
  * gcc-12 -march=sapphirerapids -dM -E - < /dev/null | egrep "SSE|AVX" | sort
42
44
  * On Arm machines you may want to check for other flags:
@@ -80,7 +82,7 @@
80
82
  * The N2 core is very similar to V2 and is used by Microsoft @b Cobalt.
81
83
  * https://developer.arm.com/Processors/Neoverse%20N2
82
84
  *
83
- * On Consumer side, Apple is the biggest player with mobile @b A chips and desktop @b M chips.
85
+ * On the consumer side, Apple is the biggest player with mobile @b A chips and desktop @b M chips.
84
86
  * The M1 implements Armv8.5-A, both M2 and M3 implement Armv8.6-A, and M4 is expected to have Armv9.1-A.
85
87
  */
86
88
 
@@ -89,7 +91,7 @@
89
91
 
90
92
  #define SIMSIMD_VERSION_MAJOR 6
91
93
  #define SIMSIMD_VERSION_MINOR 5
92
- #define SIMSIMD_VERSION_PATCH 1
94
+ #define SIMSIMD_VERSION_PATCH 5
93
95
 
94
96
  /**
95
97
  * @brief Removes compile-time dispatching, and replaces it with runtime dispatching.
@@ -116,6 +118,22 @@
116
118
  #include <sys/sysctl.h> // `sysctlbyname`
117
119
  #endif
118
120
 
121
+ // Detect POSIX extensions availability for signal handling.
122
+ // POSIX extensions provide `sigaction`, `sigjmp_buf`, and `sigsetjmp` for safe signal handling.
123
+ // These are needed on Linux ARM for safely testing `mrs` instruction availability.
124
+ #if defined(_SIMSIMD_DEFINED_LINUX) && defined(_POSIX_VERSION)
125
+ #include <setjmp.h> // `sigjmp_buf`, `sigsetjmp`, `siglongjmp`
126
+ #include <signal.h> // `sigaction`, `SIGILL`
127
+ #define _SIMSIMD_HAS_POSIX_EXTENSIONS 1
128
+ #else
129
+ #define _SIMSIMD_HAS_POSIX_EXTENSIONS 0
130
+ #endif
131
+
132
+ // On Windows ARM, we use IsProcessorFeaturePresent API for capability detection
133
+ #if defined(_SIMSIMD_DEFINED_WINDOWS) && _SIMSIMD_TARGET_ARM
134
+ #include <processthreadsapi.h> // `IsProcessorFeaturePresent`
135
+ #endif
136
+
119
137
  #ifdef __cplusplus
120
138
  extern "C" {
121
139
  #endif
@@ -454,6 +472,15 @@ SIMSIMD_PUBLIC simsimd_capability_t _simsimd_capabilities_x86(void) {
454
472
  #pragma GCC target("arch=armv8.5-a+sve")
455
473
  #pragma clang attribute push(__attribute__((target("arch=armv8.5-a+sve"))), apply_to = function)
456
474
 
475
+ #if _SIMSIMD_HAS_POSIX_EXTENSIONS
476
+ /** @brief SIGILL handler for `mrs` instruction testing on Linux ARM */
477
+ static sigjmp_buf _simsimd_mrs_test_jump_buffer;
478
+ static void _simsimd_mrs_test_sigill_handler(int sig) {
479
+ (void)sig; // Unused parameter
480
+ siglongjmp(_simsimd_mrs_test_jump_buffer, 1);
481
+ }
482
+ #endif
483
+
457
484
  /**
458
485
  * @brief Function to flush denormalized numbers to zero on Arm CPUs.
459
486
  * @note This should be called on each thread before any SIMD operations to avoid performance penalties.
@@ -504,6 +531,36 @@ SIMSIMD_PUBLIC simsimd_capability_t _simsimd_capabilities_arm(void) {
504
531
 
505
532
  #elif defined(_SIMSIMD_DEFINED_LINUX)
506
533
 
534
+ // Depending on the environment, reading system registers may cause SIGILL.
535
+ // One option to avoid the crash is to use `getauxval(AT_HWCAP)` and `getauxval(AT_HWCAP2)`,
536
+ // Linux APIs, but those aren't as informative as reading the registers directly.
537
+ // So before reading the ID registers, we set up a signal handler to catch SIGILL
538
+ // and probe one of the registers, reverting back to the old signal handler afterwards.
539
+ //
540
+ // This issue was originally observed in: https://github.com/ashvardanian/SimSIMD/issues/279
541
+ #if _SIMSIMD_HAS_POSIX_EXTENSIONS
542
+ struct sigaction action_new, action_old;
543
+ action_new.sa_handler = _simsimd_mrs_test_sigill_handler;
544
+ sigemptyset(&action_new.sa_mask);
545
+ action_new.sa_flags = 0;
546
+
547
+ int mrs_works = 0;
548
+ if (sigaction(SIGILL, &action_new, &action_old) == 0) {
549
+ if (sigsetjmp(_simsimd_mrs_test_jump_buffer, 1) == 0) {
550
+ unsigned long midr_value;
551
+ __asm__ __volatile__("mrs %0, MIDR_EL1" : "=r"(midr_value));
552
+ mrs_works = 1;
553
+ }
554
+ sigaction(SIGILL, &action_old, NULL);
555
+ }
556
+
557
+ // Early exit if `mrs` doesn't work - return conservative NEON-only capabilities
558
+ if (!mrs_works) return (simsimd_capability_t)(simsimd_cap_neon_k | simsimd_cap_serial_k);
559
+ #else // _SIMSIMD_HAS_POSIX_EXTENSIONS
560
+ // Without POSIX signal handlers, fall back to conservative NEON capabilities.
561
+ return (simsimd_capability_t)(simsimd_cap_neon_k | simsimd_cap_serial_k);
562
+ #endif // _SIMSIMD_HAS_POSIX_EXTENSIONS
563
+
507
564
  // Read CPUID registers directly
508
565
  unsigned long id_aa64isar0_el1 = 0, id_aa64isar1_el1 = 0, id_aa64pfr0_el1 = 0, id_aa64zfr0_el1 = 0;
509
566
 
@@ -561,8 +618,31 @@ SIMSIMD_PUBLIC simsimd_capability_t _simsimd_capabilities_arm(void) {
561
618
  (simsimd_cap_sve2_k * (supports_sve2)) | //
562
619
  (simsimd_cap_sve2p1_k * (supports_sve2p1)) | //
563
620
  (simsimd_cap_serial_k));
564
- #else // if !_SIMSIMD_DEFINED_LINUX
565
- return simsimd_cap_serial_k;
621
+
622
+ #elif defined(_SIMSIMD_DEFINED_WINDOWS)
623
+
624
+ unsigned supports_neon = 0, supports_dp = 0;
625
+
626
+ // On Windows ARM, use the `IsProcessorFeaturePresent` API for capability detection.
627
+ // https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-isprocessorfeaturepresent
628
+ #if defined(PF_ARM_V8_INSTRUCTIONS_AVAILABLE)
629
+ supports_neon = IsProcessorFeaturePresent(PF_ARM_V8_INSTRUCTIONS_AVAILABLE);
630
+ #endif
631
+ #if defined(PF_ARM_V82_DP_INSTRUCTIONS_AVAILABLE)
632
+ supports_dp = IsProcessorFeaturePresent(PF_ARM_V82_DP_INSTRUCTIONS_AVAILABLE);
633
+ #endif
634
+
635
+ // Windows API doesn't provide reliable detection for FP16, BF16.
636
+ return (simsimd_capability_t)( //
637
+ (simsimd_cap_neon_k * (supports_neon)) | //
638
+ (simsimd_cap_neon_i8_k * (supports_neon && supports_dp)) | //
639
+ (simsimd_cap_serial_k));
640
+
641
+ #else // Unknown platform
642
+
643
+ // Conservative fallback for unknown platforms: NEON is mandatory in ARMv8-A (ARM64)
644
+ return (simsimd_capability_t)(simsimd_cap_neon_k | simsimd_cap_serial_k);
645
+
566
646
  #endif
567
647
  }
568
648
 
@@ -410,7 +410,7 @@ SIMSIMD_PUBLIC void simsimd_intersect_u16_ice( //
410
410
  simsimd_u8_t u8[64];
411
411
  } a_vec, b_vec;
412
412
 
413
- while (a + 32 < a_end && b + 32 < b_end) {
413
+ while (a + 32 <= a_end && b + 32 <= b_end) {
414
414
  a_vec.zmm = _mm512_loadu_si512((__m512i const *)a);
415
415
  b_vec.zmm = _mm512_loadu_si512((__m512i const *)b);
416
416
 
@@ -422,13 +422,13 @@ SIMSIMD_PUBLIC void simsimd_intersect_u16_ice( //
422
422
  simsimd_u16_t b_max = b_vec.u16[31];
423
423
 
424
424
  // If the slices don't overlap, advance the appropriate pointer
425
- while (a_max < b_min && a + 64 < a_end) {
425
+ while (a_max < b_min && a + 64 <= a_end) {
426
426
  a += 32;
427
427
  a_vec.zmm = _mm512_loadu_si512((__m512i const *)a);
428
428
  a_max = a_vec.u16[31];
429
429
  }
430
430
  a_min = a_vec.u16[0];
431
- while (b_max < a_min && b + 64 < b_end) {
431
+ while (b_max < a_min && b + 64 <= b_end) {
432
432
  b += 32;
433
433
  b_vec.zmm = _mm512_loadu_si512((__m512i const *)b);
434
434
  b_max = b_vec.u16[31];
@@ -475,7 +475,7 @@ SIMSIMD_PUBLIC void simsimd_intersect_u32_ice( //
475
475
  simsimd_u8_t u8[64];
476
476
  } a_vec, b_vec;
477
477
 
478
- while (a + 16 < a_end && b + 16 < b_end) {
478
+ while (a + 16 <= a_end && b + 16 <= b_end) {
479
479
  a_vec.zmm = _mm512_loadu_si512((__m512i const *)a);
480
480
  b_vec.zmm = _mm512_loadu_si512((__m512i const *)b);
481
481
 
@@ -487,13 +487,13 @@ SIMSIMD_PUBLIC void simsimd_intersect_u32_ice( //
487
487
  simsimd_u32_t b_max = b_vec.u32[15];
488
488
 
489
489
  // If the slices don't overlap, advance the appropriate pointer
490
- while (a_max < b_min && a + 32 < a_end) {
490
+ while (a_max < b_min && a + 32 <= a_end) {
491
491
  a += 16;
492
492
  a_vec.zmm = _mm512_loadu_si512((__m512i const *)a);
493
493
  a_max = a_vec.u32[15];
494
494
  }
495
495
  a_min = a_vec.u32[0];
496
- while (b_max < a_min && b + 32 < b_end) {
496
+ while (b_max < a_min && b + 32 <= b_end) {
497
497
  b += 16;
498
498
  b_vec.zmm = _mm512_loadu_si512((__m512i const *)b);
499
499
  b_max = b_vec.u32[15];
@@ -526,11 +526,11 @@ SIMSIMD_PUBLIC void simsimd_intersect_u32_ice( //
526
526
 
527
527
  #if SIMSIMD_TARGET_TURIN
528
528
  #pragma GCC push_options
529
- #pragma GCC target("avx2", "avx512f", "avx512vl", "bmi2", "lzcnt", "popcnt", "avx512bw", "avx512vbmi2", "avx512bf16", \
530
- "avx512vnni", "avx512vp2intersect", "avx512dq")
531
- #pragma clang attribute push( \
532
- __attribute__((target( \
533
- "avx2,avx512f,avx512vl,bmi2,lzcnt,popcnt,avx512bw,avx512vbmi2,avx512bf16,avx512vnni,avx512vp2intersect,avx512dq"))), \
529
+ #pragma GCC target("avx2", "avx512f", "avx512vl", "bmi", "bmi2", "lzcnt", "popcnt", "avx512bw", "avx512vbmi2", \
530
+ "avx512bf16", "avx512vnni", "avx512vp2intersect", "avx512dq")
531
+ #pragma clang attribute push( \
532
+ __attribute__((target( \
533
+ "avx2,avx512f,avx512vl,bmi,bmi2,lzcnt,popcnt,avx512bw,avx512vbmi2,avx512bf16,avx512vnni,avx512vp2intersect,avx512dq"))), \
534
534
  apply_to = function)
535
535
 
536
536
  SIMSIMD_PUBLIC void simsimd_intersect_u16_turin( //
@@ -538,12 +538,6 @@ SIMSIMD_PUBLIC void simsimd_intersect_u16_turin( //
538
538
  simsimd_size_t a_length, simsimd_size_t b_length, //
539
539
  simsimd_distance_t *results) {
540
540
 
541
- // The baseline implementation for very small arrays (2 registers or less) can be quite simple:
542
- if (a_length < 64 && b_length < 64) {
543
- simsimd_intersect_u16_serial(a, b, a_length, b_length, results);
544
- return;
545
- }
546
-
547
541
  //! There is no such thing as `_mm512_2intersect_epi16`, only the 32-bit variant!
548
542
  //! So instead of jumping through 32 entries at a time, like on Ice Lake, we will
549
543
  //! step through 16 entries at a time.
@@ -556,32 +550,13 @@ SIMSIMD_PUBLIC void simsimd_intersect_u16_turin( //
556
550
  simsimd_u8_t u8[32];
557
551
  } a_vec, b_vec;
558
552
 
559
- while (a + 16 < a_end && b + 16 < b_end) {
553
+ // Broadcast index for last element (hoisted outside loop)
554
+ __m256i const last_idx = _mm256_set1_epi16(15);
555
+ while (a + 16 <= a_end && b + 16 <= b_end) {
560
556
  a_vec.ymm = _mm256_lddqu_si256((__m256i const *)a);
561
557
  b_vec.ymm = _mm256_lddqu_si256((__m256i const *)b);
562
558
 
563
- // Intersecting registers with `_mm512_2intersect_epi16_mask` involves a lot of shuffling
564
- // and comparisons, so we want to avoid it if the slices don't overlap at all..
565
- simsimd_u16_t a_min;
566
- simsimd_u16_t a_max = a_vec.u16[15];
567
- simsimd_u16_t b_min = b_vec.u16[0];
568
- simsimd_u16_t b_max = b_vec.u16[15];
569
-
570
- // If the slices don't overlap, advance the appropriate pointer
571
- while (a_max < b_min && a + 32 < a_end) {
572
- a += 16;
573
- a_vec.ymm = _mm256_lddqu_si256((__m256i const *)a);
574
- a_max = a_vec.u16[15];
575
- }
576
- a_min = a_vec.u16[0];
577
- while (b_max < a_min && b + 32 < b_end) {
578
- b += 16;
579
- b_vec.ymm = _mm256_lddqu_si256((__m256i const *)b);
580
- b_max = b_vec.u16[15];
581
- }
582
- b_min = b_vec.u16[0];
583
-
584
- // Now we are likely to have some overlap, so we can intersect the registers
559
+ // Intersect the registers
585
560
  __m512i a_i32_vec = _mm512_cvtepu16_epi32(a_vec.ymm);
586
561
  __m512i b_i32_vec = _mm512_cvtepu16_epi32(b_vec.ymm);
587
562
  __mmask16 a_matches_any_in_b, b_matches_any_in_a;
@@ -592,12 +567,12 @@ SIMSIMD_PUBLIC void simsimd_intersect_u16_turin( //
592
567
  // _mm512_mask_compressstoreu_epi16(c, a_matches_any_in_b, a_vec);
593
568
  c += _mm_popcnt_u32(a_matches_any_in_b); // MSVC has no `_popcnt32`
594
569
 
595
- __m256i a_last_broadcasted = _mm256_set1_epi16(*(short const *)&a_max);
596
- __m256i b_last_broadcasted = _mm256_set1_epi16(*(short const *)&b_max);
570
+ __m256i a_last_broadcasted = _mm256_permutexvar_epi16(last_idx, a_vec.ymm);
571
+ __m256i b_last_broadcasted = _mm256_permutexvar_epi16(last_idx, b_vec.ymm);
597
572
  __mmask16 a_step_mask = _mm256_cmple_epu16_mask(a_vec.ymm, b_last_broadcasted);
598
573
  __mmask16 b_step_mask = _mm256_cmple_epu16_mask(b_vec.ymm, a_last_broadcasted);
599
- a += 32 - _lzcnt_u32((simsimd_u32_t)a_step_mask); //? Is this correct? Needs testing!
600
- b += 32 - _lzcnt_u32((simsimd_u32_t)b_step_mask);
574
+ a += _tzcnt_u32(~(simsimd_u32_t)a_step_mask | 0x10000);
575
+ b += _tzcnt_u32(~(simsimd_u32_t)b_step_mask | 0x10000);
601
576
  }
602
577
 
603
578
  simsimd_intersect_u16_serial(a, b, a_end - a, b_end - b, results);
@@ -609,12 +584,6 @@ SIMSIMD_PUBLIC void simsimd_intersect_u32_turin( //
609
584
  simsimd_size_t a_length, simsimd_size_t b_length, //
610
585
  simsimd_distance_t *results) {
611
586
 
612
- // The baseline implementation for very small arrays (2 registers or less) can be quite simple:
613
- if (a_length < 32 && b_length < 32) {
614
- simsimd_intersect_u32_serial(a, b, a_length, b_length, results);
615
- return;
616
- }
617
-
618
587
  simsimd_u32_t const *const a_end = a + a_length;
619
588
  simsimd_u32_t const *const b_end = b + b_length;
620
589
  simsimd_size_t c = 0;
@@ -624,32 +593,13 @@ SIMSIMD_PUBLIC void simsimd_intersect_u32_turin( //
624
593
  simsimd_u8_t u8[64];
625
594
  } a_vec, b_vec;
626
595
 
627
- while (a + 16 < a_end && b + 16 < b_end) {
596
+ // Broadcast index for last element (hoisted outside loop)
597
+ __m512i const last_idx = _mm512_set1_epi32(15);
598
+ while (a + 16 <= a_end && b + 16 <= b_end) {
628
599
  a_vec.zmm = _mm512_loadu_si512((__m512i const *)a);
629
600
  b_vec.zmm = _mm512_loadu_si512((__m512i const *)b);
630
601
 
631
- // Intersecting registers with `_mm512_2intersect_epi32` involves a lot of shuffling
632
- // and comparisons, so we want to avoid it if the slices don't overlap at all..
633
- simsimd_u32_t a_min;
634
- simsimd_u32_t a_max = a_vec.u32[15];
635
- simsimd_u32_t b_min = b_vec.u32[0];
636
- simsimd_u32_t b_max = b_vec.u32[15];
637
-
638
- // If the slices don't overlap, advance the appropriate pointer
639
- while (a_max < b_min && a + 32 < a_end) {
640
- a += 16;
641
- a_vec.zmm = _mm512_loadu_si512((__m512i const *)a);
642
- a_max = a_vec.u32[15];
643
- }
644
- a_min = a_vec.u32[0];
645
- while (b_max < a_min && b + 32 < b_end) {
646
- b += 16;
647
- b_vec.zmm = _mm512_loadu_si512((__m512i const *)b);
648
- b_max = b_vec.u32[15];
649
- }
650
- b_min = b_vec.u32[0];
651
-
652
- // Now we are likely to have some overlap, so we can intersect the registers
602
+ // Intersect the registers
653
603
  __mmask16 a_matches_any_in_b, b_matches_any_in_a;
654
604
  _mm512_2intersect_epi32(a_vec.zmm, b_vec.zmm, &a_matches_any_in_b, &b_matches_any_in_a);
655
605
 
@@ -658,12 +608,13 @@ SIMSIMD_PUBLIC void simsimd_intersect_u32_turin( //
658
608
  // _mm512_mask_compressstoreu_epi32(c, a_matches_any_in_b, a_vec);
659
609
  c += _mm_popcnt_u32(a_matches_any_in_b); // MSVC has no `_popcnt32`
660
610
 
661
- __m512i a_last_broadcasted = _mm512_set1_epi32(*(int const *)&a_max);
662
- __m512i b_last_broadcasted = _mm512_set1_epi32(*(int const *)&b_max);
611
+ // Pure SIMD broadcasts - no scalar extraction needed
612
+ __m512i a_last_broadcasted = _mm512_permutexvar_epi32(last_idx, a_vec.zmm);
613
+ __m512i b_last_broadcasted = _mm512_permutexvar_epi32(last_idx, b_vec.zmm);
663
614
  __mmask16 a_step_mask = _mm512_cmple_epu32_mask(a_vec.zmm, b_last_broadcasted);
664
615
  __mmask16 b_step_mask = _mm512_cmple_epu32_mask(b_vec.zmm, a_last_broadcasted);
665
- a += 32 - _lzcnt_u32((simsimd_u32_t)a_step_mask);
666
- b += 32 - _lzcnt_u32((simsimd_u32_t)b_step_mask);
616
+ a += _tzcnt_u32(~(simsimd_u32_t)a_step_mask | 0x10000);
617
+ b += _tzcnt_u32(~(simsimd_u32_t)b_step_mask | 0x10000);
667
618
  }
668
619
 
669
620
  simsimd_intersect_u32_serial(a, b, a_end - a, b_end - b, results);
@@ -696,7 +647,9 @@ SIMSIMD_PUBLIC void simsimd_spdot_weights_u16_turin( //
696
647
  } a_vec, b_vec, product_vec;
697
648
  product_vec.ymmps = _mm256_setzero_ps();
698
649
 
699
- while (a + 16 < a_end && b + 16 < b_end) {
650
+ // Broadcast index for last element (hoisted outside loop)
651
+ __m256i const last_idx = _mm256_set1_epi16(15);
652
+ while (a + 16 <= a_end && b + 16 <= b_end) {
700
653
  a_vec.ymm = _mm256_lddqu_si256((__m256i const *)a);
701
654
  b_vec.ymm = _mm256_lddqu_si256((__m256i const *)b);
702
655
 
@@ -708,13 +661,13 @@ SIMSIMD_PUBLIC void simsimd_spdot_weights_u16_turin( //
708
661
  simsimd_u16_t b_max = b_vec.u16[15];
709
662
 
710
663
  // If the slices don't overlap, advance the appropriate pointer
711
- while (a_max < b_min && a + 32 < a_end) {
664
+ while (a_max < b_min && a + 32 <= a_end) {
712
665
  a += 16, a_weights += 16;
713
666
  a_vec.ymm = _mm256_lddqu_si256((__m256i const *)a);
714
667
  a_max = a_vec.u16[15];
715
668
  }
716
669
  a_min = a_vec.u16[0];
717
- while (b_max < a_min && b + 32 < b_end) {
670
+ while (b_max < a_min && b + 32 <= b_end) {
718
671
  b += 16, b_weights += 16;
719
672
  b_vec.ymm = _mm256_lddqu_si256((__m256i const *)b);
720
673
  b_max = b_vec.u16[15];
@@ -742,12 +695,12 @@ SIMSIMD_PUBLIC void simsimd_spdot_weights_u16_turin( //
742
695
  product_vec.ymmps = _mm256_dpbf16_ps(product_vec.ymmps, (__m256bh)a_weights_vec, (__m256bh)b_weights_vec);
743
696
  }
744
697
 
745
- __m256i a_last_broadcasted = _mm256_set1_epi16(*(short const *)&a_max);
746
- __m256i b_last_broadcasted = _mm256_set1_epi16(*(short const *)&b_max);
698
+ __m256i a_last_broadcasted = _mm256_permutexvar_epi16(last_idx, a_vec.ymm);
699
+ __m256i b_last_broadcasted = _mm256_permutexvar_epi16(last_idx, b_vec.ymm);
747
700
  __mmask16 a_step_mask = _mm256_cmple_epu16_mask(a_vec.ymm, b_last_broadcasted);
748
701
  __mmask16 b_step_mask = _mm256_cmple_epu16_mask(b_vec.ymm, a_last_broadcasted);
749
- int a_step = 32 - _lzcnt_u32((simsimd_u32_t)a_step_mask); //? Is this correct? Needs testing!
750
- int b_step = 32 - _lzcnt_u32((simsimd_u32_t)b_step_mask);
702
+ simsimd_size_t a_step = _tzcnt_u32(~(simsimd_u32_t)a_step_mask | 0x10000);
703
+ simsimd_size_t b_step = _tzcnt_u32(~(simsimd_u32_t)b_step_mask | 0x10000);
751
704
  a += a_step, a_weights += a_step;
752
705
  b += b_step, b_weights += b_step;
753
706
  }
@@ -781,7 +734,9 @@ SIMSIMD_PUBLIC void simsimd_spdot_counts_u16_turin( //
781
734
  } a_vec, b_vec, product_vec;
782
735
  product_vec.ymm = _mm256_setzero_si256();
783
736
 
784
- while (a + 16 < a_end && b + 16 < b_end) {
737
+ // Broadcast index for last element (hoisted outside loop)
738
+ __m256i const last_idx = _mm256_set1_epi16(15);
739
+ while (a + 16 <= a_end && b + 16 <= b_end) {
785
740
  a_vec.ymm = _mm256_lddqu_si256((__m256i const *)a);
786
741
  b_vec.ymm = _mm256_lddqu_si256((__m256i const *)b);
787
742
 
@@ -793,13 +748,13 @@ SIMSIMD_PUBLIC void simsimd_spdot_counts_u16_turin( //
793
748
  simsimd_u16_t b_max = b_vec.u16[15];
794
749
 
795
750
  // If the slices don't overlap, advance the appropriate pointer
796
- while (a_max < b_min && a + 32 < a_end) {
751
+ while (a_max < b_min && a + 32 <= a_end) {
797
752
  a += 16, a_weights += 16;
798
753
  a_vec.ymm = _mm256_lddqu_si256((__m256i const *)a);
799
754
  a_max = a_vec.u16[15];
800
755
  }
801
756
  a_min = a_vec.u16[0];
802
- while (b_max < a_min && b + 32 < b_end) {
757
+ while (b_max < a_min && b + 32 <= b_end) {
803
758
  b += 16, b_weights += 16;
804
759
  b_vec.ymm = _mm256_lddqu_si256((__m256i const *)b);
805
760
  b_max = b_vec.u16[15];
@@ -827,12 +782,12 @@ SIMSIMD_PUBLIC void simsimd_spdot_counts_u16_turin( //
827
782
  product_vec.ymm = _mm256_dpwssds_epi32(product_vec.ymm, a_weights_vec, b_weights_vec);
828
783
  }
829
784
 
830
- __m256i a_last_broadcasted = _mm256_set1_epi16(*(short const *)&a_max);
831
- __m256i b_last_broadcasted = _mm256_set1_epi16(*(short const *)&b_max);
785
+ __m256i a_last_broadcasted = _mm256_permutexvar_epi16(last_idx, a_vec.ymm);
786
+ __m256i b_last_broadcasted = _mm256_permutexvar_epi16(last_idx, b_vec.ymm);
832
787
  __mmask16 a_step_mask = _mm256_cmple_epu16_mask(a_vec.ymm, b_last_broadcasted);
833
788
  __mmask16 b_step_mask = _mm256_cmple_epu16_mask(b_vec.ymm, a_last_broadcasted);
834
- int a_step = 32 - _lzcnt_u32((simsimd_u32_t)a_step_mask); //? Is this correct? Needs testing!
835
- int b_step = 32 - _lzcnt_u32((simsimd_u32_t)b_step_mask);
789
+ simsimd_size_t a_step = _tzcnt_u32(~(simsimd_u32_t)a_step_mask | 0x10000);
790
+ simsimd_size_t b_step = _tzcnt_u32(~(simsimd_u32_t)b_step_mask | 0x10000);
836
791
  a += a_step, a_weights += a_step;
837
792
  b += b_step, b_weights += b_step;
838
793
  }
@@ -925,7 +880,7 @@ SIMSIMD_PUBLIC void simsimd_intersect_u16_neon( //
925
880
  } a_vec, b_vec, c_counts_vec;
926
881
  c_counts_vec.u16x8 = vdupq_n_u16(0);
927
882
 
928
- while (a + 8 < a_end && b + 8 < b_end) {
883
+ while (a + 8 <= a_end && b + 8 <= b_end) {
929
884
  a_vec.u16x8 = vld1q_u16(a);
930
885
  b_vec.u16x8 = vld1q_u16(b);
931
886
 
@@ -937,13 +892,13 @@ SIMSIMD_PUBLIC void simsimd_intersect_u16_neon( //
937
892
  simsimd_u16_t b_max = b_vec.u16[7];
938
893
 
939
894
  // If the slices don't overlap, advance the appropriate pointer
940
- while (a_max < b_min && a + 16 < a_end) {
895
+ while (a_max < b_min && a + 16 <= a_end) {
941
896
  a += 8;
942
897
  a_vec.u16x8 = vld1q_u16(a);
943
898
  a_max = a_vec.u16[7];
944
899
  }
945
900
  a_min = a_vec.u16[0];
946
- while (b_max < a_min && b + 16 < b_end) {
901
+ while (b_max < a_min && b + 16 <= b_end) {
947
902
  b += 8;
948
903
  b_vec.u16x8 = vld1q_u16(b);
949
904
  b_max = b_vec.u16[7];
@@ -1008,7 +963,7 @@ SIMSIMD_PUBLIC void simsimd_intersect_u32_neon( //
1008
963
  } a_vec, b_vec, c_counts_vec;
1009
964
  c_counts_vec.u32x4 = vdupq_n_u32(0);
1010
965
 
1011
- while (a + 4 < a_end && b + 4 < b_end) {
966
+ while (a + 4 <= a_end && b + 4 <= b_end) {
1012
967
  a_vec.u32x4 = vld1q_u32(a);
1013
968
  b_vec.u32x4 = vld1q_u32(b);
1014
969
 
@@ -1020,13 +975,13 @@ SIMSIMD_PUBLIC void simsimd_intersect_u32_neon( //
1020
975
  simsimd_u32_t b_max = b_vec.u32[3];
1021
976
 
1022
977
  // If the slices don't overlap, advance the appropriate pointer
1023
- while (a_max < b_min && a + 8 < a_end) {
978
+ while (a_max < b_min && a + 8 <= a_end) {
1024
979
  a += 4;
1025
980
  a_vec.u32x4 = vld1q_u32(a);
1026
981
  a_max = a_vec.u32[3];
1027
982
  }
1028
983
  a_min = a_vec.u32[0];
1029
- while (b_max < a_min && b + 8 < b_end) {
984
+ while (b_max < a_min && b + 8 <= b_end) {
1030
985
  b += 4;
1031
986
  b_vec.u32x4 = vld1q_u32(b);
1032
987
  b_max = b_vec.u32[3];
@@ -1118,14 +1073,14 @@ SIMSIMD_PUBLIC void simsimd_intersect_u16_sve2( //
1118
1073
  simsimd_u16_t b_max = svlastb(b_progress, b_vec);
1119
1074
 
1120
1075
  // If the slices don't overlap, advance the appropriate pointer
1121
- while (a_max < b_min && (a_idx + register_size) < a_length) {
1076
+ while (a_max < b_min && (a_idx + register_size) <= a_length) {
1122
1077
  a_idx += register_size;
1123
1078
  a_progress = svwhilelt_b16_u64(a_idx, a_length);
1124
1079
  a_vec = svld1_u16(a_progress, a + a_idx);
1125
1080
  a_max = svlastb(a_progress, a_vec);
1126
1081
  }
1127
1082
  a_min = svlasta(svpfalse_b(), a_vec);
1128
- while (b_max < a_min && (b_idx + register_size) < b_length) {
1083
+ while (b_max < a_min && (b_idx + register_size) <= b_length) {
1129
1084
  b_idx += register_size;
1130
1085
  b_progress = svwhilelt_b16_u64(b_idx, b_length);
1131
1086
  b_vec = svld1_u16(b_progress, b + b_idx);
@@ -1185,14 +1140,14 @@ SIMSIMD_PUBLIC void simsimd_intersect_u32_sve2(simsimd_u32_t const *a, simsimd_u
1185
1140
  simsimd_u32_t b_max = svlastb(b_progress, b_vec);
1186
1141
 
1187
1142
  // If the slices don't overlap, advance the appropriate pointer
1188
- while (a_max < b_min && (a_idx + register_size) < a_length) {
1143
+ while (a_max < b_min && (a_idx + register_size) <= a_length) {
1189
1144
  a_idx += register_size;
1190
1145
  a_progress = svwhilelt_b32_u64(a_idx, a_length);
1191
1146
  a_vec = svld1_u32(a_progress, a + a_idx);
1192
1147
  a_max = svlastb(a_progress, a_vec);
1193
1148
  }
1194
1149
  a_min = svlasta(svpfalse_b(), a_vec);
1195
- while (b_max < a_min && (b_idx + register_size) < b_length) {
1150
+ while (b_max < a_min && (b_idx + register_size) <= b_length) {
1196
1151
  b_idx += register_size;
1197
1152
  b_progress = svwhilelt_b32_u64(b_idx, b_length);
1198
1153
  b_vec = svld1_u32(b_progress, b + b_idx);
@@ -1284,14 +1239,14 @@ SIMSIMD_PUBLIC void simsimd_spdot_counts_u16_sve2( //
1284
1239
  simsimd_u16_t b_max = svlastb(b_progress, b_vec);
1285
1240
 
1286
1241
  // If the slices don't overlap, advance the appropriate pointer
1287
- while (a_max < b_min && (a_idx + register_size) < a_length) {
1242
+ while (a_max < b_min && (a_idx + register_size) <= a_length) {
1288
1243
  a_idx += register_size;
1289
1244
  a_progress = svwhilelt_b16_u64(a_idx, a_length);
1290
1245
  a_vec = svld1_u16(a_progress, a + a_idx);
1291
1246
  a_max = svlastb(a_progress, a_vec);
1292
1247
  }
1293
1248
  a_min = svlasta(svpfalse_b(), a_vec);
1294
- while (b_max < a_min && (b_idx + register_size) < b_length) {
1249
+ while (b_max < a_min && (b_idx + register_size) <= b_length) {
1295
1250
  b_idx += register_size;
1296
1251
  b_progress = svwhilelt_b16_u64(b_idx, b_length);
1297
1252
  b_vec = svld1_u16(b_progress, b + b_idx);
@@ -1367,14 +1322,14 @@ SIMSIMD_PUBLIC void simsimd_spdot_weights_u16_sve2( //
1367
1322
  simsimd_u16_t b_max = svlastb(b_progress, b_vec);
1368
1323
 
1369
1324
  // If the slices don't overlap, advance the appropriate pointer
1370
- while (a_max < b_min && (a_idx + register_size) < a_length) {
1325
+ while (a_max < b_min && (a_idx + register_size) <= a_length) {
1371
1326
  a_idx += register_size;
1372
1327
  a_progress = svwhilelt_b16_u64(a_idx, a_length);
1373
1328
  a_vec = svld1_u16(a_progress, a + a_idx);
1374
1329
  a_max = svlastb(a_progress, a_vec);
1375
1330
  }
1376
1331
  a_min = svlasta(svpfalse_b(), a_vec);
1377
- while (b_max < a_min && (b_idx + register_size) < b_length) {
1332
+ while (b_max < a_min && (b_idx + register_size) <= b_length) {
1378
1333
  b_idx += register_size;
1379
1334
  b_progress = svwhilelt_b16_u64(b_idx, b_length);
1380
1335
  b_vec = svld1_u16(b_progress, b + b_idx);
@@ -12,7 +12,7 @@
12
12
  #ifndef SIMSIMD_TYPES_H
13
13
  #define SIMSIMD_TYPES_H
14
14
 
15
- // Inferring target OS: Windows, MacOS, or Linux
15
+ // Inferring target OS: Windows, macOS, or Linux
16
16
  #if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__)
17
17
  #define _SIMSIMD_DEFINED_WINDOWS 1
18
18
  #elif defined(__APPLE__) && defined(__MACH__)
@@ -28,7 +28,7 @@
28
28
  // - `SIMSIMD_DYNAMIC` is used for functions that are part of the public API, but are dispatched at runtime.
29
29
  //
30
30
  // On GCC we mark the functions as `nonnull` informing that none of the arguments can be `NULL`.
31
- // Marking with `pure` and `const` isn't possible as outputing to a pointer is a "side effect".
31
+ // Marking with `pure` and `const` isn't possible as outputting to a pointer is a "side effect".
32
32
  #if defined(_WIN32) || defined(__CYGWIN__)
33
33
  #define SIMSIMD_DYNAMIC __declspec(dllexport)
34
34
  #define SIMSIMD_PUBLIC inline static
@@ -259,6 +259,7 @@
259
259
  #define SIMSIMD_LOG(x) (log(x))
260
260
  #endif
261
261
 
262
+ // Copy 16 bits (2 bytes) from source to destination
262
263
  #if defined(__GNUC__) || defined(__clang__)
263
264
  #define SIMSIMD_COPY16(destination_ptr, source_ptr) __builtin_memcpy((destination_ptr), (source_ptr), 2)
264
265
  #else
@@ -395,7 +396,7 @@ typedef unsigned short simsimd_bf16_t;
395
396
 
396
397
  /*
397
398
  * Let's make sure the sizes of the types are as expected.
398
- * In C the `_Static_assert` is only available with C 11 and later.
399
+ * In C the `_Static_assert` is only available with C11 and later.
399
400
  */
400
401
  #define SIMSIMD_STATIC_ASSERT(cond, msg) typedef char static_assertion_##msg[(cond) ? 1 : -1]
401
402
  SIMSIMD_STATIC_ASSERT(sizeof(simsimd_b8_t) == 1, simsimd_b8_t_must_be_1_byte);
@@ -1,8 +1,8 @@
1
1
  /**
2
- * @brief Computes the inner distance of two vectors (same as dot product).
2
+ * @brief Computes the inner product of two vectors (dot product).
3
3
  * @param {Float64Array|Float32Array} a - The first vector.
4
4
  * @param {Float64Array|Float32Array} b - The second vector.
5
- * @returns {number} The inner distance of vectors a and b.
5
+ * @returns {number} The inner product of vectors a and b.
6
6
  */
7
7
  export function inner(a: Float64Array | Float32Array, b: Float64Array | Float32Array): number {
8
8
  if (a.length !== b.length) {
@@ -17,10 +17,10 @@ export function inner(a: Float64Array | Float32Array, b: Float64Array | Float32A
17
17
  }
18
18
 
19
19
  /**
20
- * @brief Computes the inner distance of two vectors (same as inner product).
20
+ * @brief Computes the dot product of two vectors (same as inner product).
21
21
  * @param {Float64Array|Float32Array} a - The first vector.
22
22
  * @param {Float64Array|Float32Array} b - The second vector.
23
- * @returns {number} The inner distance of vectors a and b.
23
+ * @returns {number} The dot product of vectors a and b.
24
24
  */
25
25
  export function dot(a: Float64Array | Float32Array, b: Float64Array | Float32Array): number {
26
26
  return inner(a, b);
@@ -51,7 +51,7 @@ export function sqeuclidean(
51
51
  * @brief Computes the L2 Euclidean distance between two vectors.
52
52
  * @param {Float64Array|Float32Array|Int8Array | Uint8Array} a - The first vector.
53
53
  * @param {Float64Array|Float32Array|Int8Array | Uint8Array} b - The second vector.
54
- * @returns {number} The L2 euclidean distance between vectors a and b.
54
+ * @returns {number} The L2 Euclidean distance between vectors a and b.
55
55
  */
56
56
  export function euclidean(
57
57
  a: Float64Array | Float32Array | Int8Array | Uint8Array,
@@ -96,12 +96,8 @@ export function cosine(
96
96
  magnitudeA = Math.sqrt(magnitudeA);
97
97
  magnitudeB = Math.sqrt(magnitudeB);
98
98
 
99
- if (magnitudeA === 0 || magnitudeB === 0) {
100
- console.warn(
101
- "Warning: One of the magnitudes is zero. Cosine similarity is undefined."
102
- );
103
- return 0;
104
- }
99
+ if (magnitudeA === 0 && magnitudeB === 0) return 0; // distance when both zero
100
+ if (magnitudeA === 0 || magnitudeB === 0) return 1; // distance when one is zero
105
101
 
106
102
  return 1 - dotProduct / (magnitudeA * magnitudeB);
107
103
  }
@@ -133,10 +129,10 @@ export const hamming = (a: Uint8Array, b: Uint8Array): number => {
133
129
  };
134
130
 
135
131
  /**
136
- * @brief Computes the bitwise Jaccard similarity coefficient between two vectors.
132
+ * @brief Computes the bitwise Jaccard distance between two vectors.
137
133
  * @param {Uint8Array} a - The first vector.
138
134
  * @param {Uint8Array} b - The second vector.
139
- * @returns {number} The Jaccard similarity coefficient between vectors a and b.
135
+ * @returns {number} The Jaccard distance between vectors a and b.
140
136
  */
141
137
  export const jaccard = (a: Uint8Array, b: Uint8Array): number => {
142
138
  if (a.length !== b.length) {
@@ -174,7 +170,7 @@ export const jaccard = (a: Uint8Array, b: Uint8Array): number => {
174
170
  * @brief Computes the Kullback-Leibler divergence between two probability distributions.
175
171
  * @param {Float64Array|Float32Array} a - The first vector.
176
172
  * @param {Float64Array|Float32Array} b - The second vector.
177
- * @returns {number} The Jaccard similarity coefficient between vectors a and b.
173
+ * @returns {number} The Kullback-Leibler divergence between vectors a and b.
178
174
  */
179
175
  export const kullbackleibler = (a: Float64Array | Float32Array, b: Float64Array | Float32Array): number => {
180
176
  if (a.length !== b.length) {