usearch 2.26.1 → 2.26.2

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 (146) hide show
  1. package/README.md +1 -1
  2. package/include/usearch/index.hpp +5 -1
  3. package/numkong/CITATION.cff +13 -9
  4. package/numkong/CONTRIBUTING.md +29 -24
  5. package/numkong/Cargo.toml +1 -1
  6. package/numkong/README.md +21 -7
  7. package/numkong/binding.gyp +47 -31
  8. package/numkong/build.rs +15 -5
  9. package/numkong/c/dispatch.h +0 -1
  10. package/numkong/c/parallel.c +139 -0
  11. package/numkong/c/parallel.h +47 -0
  12. package/numkong/cmake/toolchain-wasi.cmake +39 -5
  13. package/numkong/include/README.md +24 -17
  14. package/numkong/include/numkong/attention/sapphireamx.h +6 -6
  15. package/numkong/include/numkong/attention/sme.h +2 -2
  16. package/numkong/include/numkong/capabilities.h +2 -2
  17. package/numkong/include/numkong/cast/README.md +9 -7
  18. package/numkong/include/numkong/cast/loongsonasx.h +48 -40
  19. package/numkong/include/numkong/cast/serial.h +89 -11
  20. package/numkong/include/numkong/cast.h +3 -3
  21. package/numkong/include/numkong/curved/README.md +10 -10
  22. package/numkong/include/numkong/curved/neon.h +1 -0
  23. package/numkong/include/numkong/curved/neonbfdot.h +1 -0
  24. package/numkong/include/numkong/curved/smef64.h +1 -0
  25. package/numkong/include/numkong/dot/README.md +18 -18
  26. package/numkong/include/numkong/dot/alder.h +8 -8
  27. package/numkong/include/numkong/dot/haswell.h +8 -8
  28. package/numkong/include/numkong/dot/icelake.h +12 -12
  29. package/numkong/include/numkong/dot/loongsonasx.h +6 -6
  30. package/numkong/include/numkong/dot/neon.h +2 -1
  31. package/numkong/include/numkong/dot/neonbfdot.h +1 -0
  32. package/numkong/include/numkong/dot/neonfhm.h +1 -0
  33. package/numkong/include/numkong/dot/neonsdot.h +1 -0
  34. package/numkong/include/numkong/dot/powervsx.h +3 -3
  35. package/numkong/include/numkong/dot/serial.h +19 -6
  36. package/numkong/include/numkong/dot/skylake.h +4 -4
  37. package/numkong/include/numkong/dot/svehalf.h +1 -1
  38. package/numkong/include/numkong/dot/v128relaxed.h +24 -23
  39. package/numkong/include/numkong/dots/README.md +26 -26
  40. package/numkong/include/numkong/dots/genoa.h +3 -1
  41. package/numkong/include/numkong/dots/icelake.h +1 -0
  42. package/numkong/include/numkong/dots/powervsx.h +1 -1
  43. package/numkong/include/numkong/dots/serial.h +32 -33
  44. package/numkong/include/numkong/dots/sierra.h +4 -3
  45. package/numkong/include/numkong/each/README.md +5 -3
  46. package/numkong/include/numkong/each/sapphire.h +2 -0
  47. package/numkong/include/numkong/each/skylake.h +1 -0
  48. package/numkong/include/numkong/geospatial/README.md +6 -6
  49. package/numkong/include/numkong/geospatial/v128relaxed.h +1 -0
  50. package/numkong/include/numkong/maxsim/README.md +5 -5
  51. package/numkong/include/numkong/maxsim/genoa.h +4 -0
  52. package/numkong/include/numkong/maxsim/icelake.h +1 -0
  53. package/numkong/include/numkong/maxsim/serial.h +22 -9
  54. package/numkong/include/numkong/maxsim/sme.h +3 -2
  55. package/numkong/include/numkong/mesh/README.md +7 -4
  56. package/numkong/include/numkong/mesh/genoa.h +2 -2
  57. package/numkong/include/numkong/mesh/haswell.h +8 -8
  58. package/numkong/include/numkong/mesh/neon.h +6 -6
  59. package/numkong/include/numkong/mesh/neonbfdot.h +3 -2
  60. package/numkong/include/numkong/mesh/neonfhm.h +3 -2
  61. package/numkong/include/numkong/mesh/rvv.h +4 -4
  62. package/numkong/include/numkong/mesh/serial.h +17 -17
  63. package/numkong/include/numkong/mesh/skylake.h +4 -4
  64. package/numkong/include/numkong/mesh/v128relaxed.h +4 -4
  65. package/numkong/include/numkong/mesh.hpp +2 -2
  66. package/numkong/include/numkong/numkong.h +1 -1
  67. package/numkong/include/numkong/numkong.hpp +1 -1
  68. package/numkong/include/numkong/probability/README.md +17 -15
  69. package/numkong/include/numkong/probability/skylake.h +1 -1
  70. package/numkong/include/numkong/reduce/README.md +27 -21
  71. package/numkong/include/numkong/reduce/genoa.h +2 -1
  72. package/numkong/include/numkong/reduce/icelake.h +1 -0
  73. package/numkong/include/numkong/reduce/neon.h +1 -1
  74. package/numkong/include/numkong/reduce/serial.h +9 -9
  75. package/numkong/include/numkong/reduce/skylake.h +4 -4
  76. package/numkong/include/numkong/reduce.hpp +38 -0
  77. package/numkong/include/numkong/scalar/README.md +29 -28
  78. package/numkong/include/numkong/set/README.md +17 -14
  79. package/numkong/include/numkong/set/haswell.h +15 -16
  80. package/numkong/include/numkong/set/icelake.h +15 -16
  81. package/numkong/include/numkong/set/loongsonasx.h +12 -10
  82. package/numkong/include/numkong/set/neon.h +15 -19
  83. package/numkong/include/numkong/set/powervsx.h +15 -21
  84. package/numkong/include/numkong/set/serial.h +19 -20
  85. package/numkong/include/numkong/set/v128relaxed.h +10 -8
  86. package/numkong/include/numkong/sets/icelake.h +1 -0
  87. package/numkong/include/numkong/sparse/README.md +4 -4
  88. package/numkong/include/numkong/sparse/icelake.h +1 -0
  89. package/numkong/include/numkong/sparse/neon.h +1 -0
  90. package/numkong/include/numkong/sparse/serial.h +10 -3
  91. package/numkong/include/numkong/sparse/turin.h +1 -0
  92. package/numkong/include/numkong/spatial/README.md +25 -24
  93. package/numkong/include/numkong/spatial/alder.h +2 -1
  94. package/numkong/include/numkong/spatial/haswell.h +48 -40
  95. package/numkong/include/numkong/spatial/icelake.h +2 -1
  96. package/numkong/include/numkong/spatial/neon.h +54 -46
  97. package/numkong/include/numkong/spatial/neonbfdot.h +1 -1
  98. package/numkong/include/numkong/spatial/powervsx.h +54 -46
  99. package/numkong/include/numkong/spatial/serial.h +56 -46
  100. package/numkong/include/numkong/spatial/sierra.h +4 -3
  101. package/numkong/include/numkong/spatial/skylake.h +27 -22
  102. package/numkong/include/numkong/spatial/sve.h +6 -6
  103. package/numkong/include/numkong/spatial/svebfdot.h +1 -1
  104. package/numkong/include/numkong/spatial/svehalf.h +1 -1
  105. package/numkong/include/numkong/spatial/v128relaxed.h +36 -30
  106. package/numkong/include/numkong/spatials/README.md +15 -15
  107. package/numkong/include/numkong/spatials/genoa.h +3 -0
  108. package/numkong/include/numkong/spatials/graniteamx.h +2 -0
  109. package/numkong/include/numkong/spatials/icelake.h +1 -0
  110. package/numkong/include/numkong/spatials/sierra.h +1 -0
  111. package/numkong/include/numkong/tensor.hpp +236 -123
  112. package/numkong/include/numkong/trigonometry/README.md +4 -4
  113. package/numkong/include/numkong/trigonometry/serial.h +6 -6
  114. package/numkong/include/numkong/trigonometry/v128relaxed.h +1 -0
  115. package/numkong/include/numkong/types.hpp +46 -16
  116. package/numkong/include/numkong/vector.hpp +116 -23
  117. package/numkong/javascript/@numkong-darwin-arm64/package.json +1 -1
  118. package/numkong/javascript/@numkong-darwin-x64/package.json +1 -1
  119. package/numkong/javascript/@numkong-linux-arm64/package.json +1 -1
  120. package/numkong/javascript/@numkong-linux-x64/package.json +1 -1
  121. package/numkong/javascript/@numkong-win32-arm64/package.json +1 -1
  122. package/numkong/javascript/@numkong-win32-x64/package.json +1 -1
  123. package/numkong/javascript/README.md +18 -16
  124. package/numkong/javascript/numkong.c +70 -40
  125. package/numkong/package.json +8 -8
  126. package/numkong/rust/README.md +19 -9
  127. package/numkong/rust/cast.rs +52 -1
  128. package/numkong/rust/each.rs +480 -7
  129. package/numkong/rust/matrix.rs +877 -216
  130. package/numkong/rust/numkong.rs +12 -8
  131. package/numkong/rust/reduce.rs +433 -5
  132. package/numkong/rust/tensor.rs +450 -842
  133. package/numkong/rust/vector.rs +520 -101
  134. package/numkong/swift/README.md +5 -3
  135. package/numkong/test/README.md +4 -4
  136. package/numkong/test/test-wasi.mjs +17 -0
  137. package/numkong/test/test_each.cpp +30 -1
  138. package/numkong/test/test_each.py +74 -1
  139. package/numkong/test/test_reduce.cpp +51 -0
  140. package/numkong/test/test_tensor.cpp +2 -63
  141. package/numkong/test/test_tensor.py +93 -0
  142. package/package.json +1 -1
  143. package/prebuilds/darwin-arm64+x64/usearch.node +0 -0
  144. package/prebuilds/linux-arm64/usearch.node +0 -0
  145. package/prebuilds/linux-x64/usearch.node +0 -0
  146. package/prebuilds/win32-x64/usearch.node +0 -0
@@ -953,7 +953,7 @@ NK_PUBLIC void nk_reduce_moments_f64_skylake( //
953
953
  NK_INTERNAL void nk_reduce_moments_i8_skylake_contiguous_( //
954
954
  nk_i8_t const *data_ptr, nk_size_t count, //
955
955
  nk_i64_t *sum_ptr, nk_u64_t *sumsq_ptr) {
956
- // Sum: VPSADBW with XOR bias (same as nk_reduce_add_i8_skylake_contiguous_).
956
+ // Sum: VPSADBW with XOR bias.
957
957
  // Sumsq: widen i8→i16, VPMADDWD(x,x) → i32 (pairs of squares), accumulate i32.
958
958
  // i32 overflow safe: max per lane = (128² + 128²) * 65536 iters ≈ 2.1B = safe limit.
959
959
  // The dispatch recurses at (NK_U16_MAX+1)*64 elements → at most 65536 iterations here.
@@ -1147,7 +1147,7 @@ NK_PUBLIC void nk_reduce_minmax_i8_skylake( //
1147
1147
  NK_INTERNAL void nk_reduce_moments_u8_skylake_contiguous_( //
1148
1148
  nk_u8_t const *data_ptr, nk_size_t count, //
1149
1149
  nk_u64_t *sum_ptr, nk_u64_t *sumsq_ptr) {
1150
- // Sum: VPSADBW directly (same as nk_reduce_add_u8_skylake_contiguous_).
1150
+ // Sum: VPSADBW directly.
1151
1151
  // Sumsq: widen u8→i16, VPMADDWD(x,x) → i32 (pairs of squares), accumulate i32.
1152
1152
  // i32 overflow safe: max per lane = (255² + 255²) * 1024 iters ≈ 133M < 2.1B.
1153
1153
  __m512i zero_u8x64 = _mm512_setzero_si512();
@@ -1697,7 +1697,7 @@ NK_PUBLIC void nk_reduce_minmax_u16_skylake( //
1697
1697
  max_index_ptr);
1698
1698
  }
1699
1699
 
1700
- /** @brief Unsigned saturating add of two i64x8 vectors (3 uops). */
1700
+ /** @brief Unsigned saturating add of two u64x8 vectors (3 uops). */
1701
1701
  NK_INTERNAL __m512i nk_u64_sadd_epi64_skylake_(__m512i a, __m512i b) {
1702
1702
  __m512i result_u64x8 = _mm512_add_epi64(a, b);
1703
1703
  __mmask8 ovf = _mm512_cmp_epu64_mask(result_u64x8, a, _MM_CMPINT_LT);
@@ -3371,7 +3371,7 @@ NK_PUBLIC void nk_reduce_moments_u4_skylake( //
3371
3371
  NK_INTERNAL void nk_reduce_moments_u1_skylake_contiguous_( //
3372
3372
  nk_u1x8_t const *data_ptr, nk_size_t count, //
3373
3373
  nk_u64_t *sum_ptr, nk_u64_t *sumsq_ptr) {
3374
- // Sum = popcount via 4-bit LUT (same as nk_reduce_add_u1_skylake). Sumsq = sum for bits.
3374
+ // Sum = popcount via 4-bit LUT. Sumsq = sum for bits.
3375
3375
  __m512i lut_i8x64 = _mm512_set_epi8( //
3376
3376
  4, 3, 3, 2, 3, 2, 2, 1, 3, 2, 2, 1, 2, 1, 1, 0, //
3377
3377
  4, 3, 3, 2, 3, 2, 2, 1, 3, 2, 2, 1, 2, 1, 1, 0, //
@@ -629,6 +629,44 @@ std::size_t argmax(vector_view<value_type_> input) noexcept {
629
629
  return minmax(input).max_index;
630
630
  }
631
631
 
632
+ /** @brief Population count of a packed bit tensor — number of set bits.
633
+ * Wraps `nk_reduce_moments_u1` via `sum`; returns 0 for empty inputs. */
634
+ template <std::size_t max_rank_ = 8>
635
+ u64_t popcount(tensor_view<u1x8_t, max_rank_> input) noexcept {
636
+ return sum<u1x8_t, max_rank_>(input);
637
+ }
638
+
639
+ /** @brief True if any bit of the packed bit tensor is set. Suffixed `_set` to
640
+ * avoid colliding with the `all_t`/`all` slice marker in `vector.hpp`. */
641
+ template <std::size_t max_rank_ = 8>
642
+ bool any_set(tensor_view<u1x8_t, max_rank_> input) noexcept {
643
+ return popcount<max_rank_>(input).raw_ != 0;
644
+ }
645
+
646
+ /** @brief True if no bit of the packed bit tensor is set. */
647
+ template <std::size_t max_rank_ = 8>
648
+ bool none_set(tensor_view<u1x8_t, max_rank_> input) noexcept {
649
+ return !any_set<max_rank_>(input);
650
+ }
651
+
652
+ /** @brief True if every bit of the packed bit tensor is set. */
653
+ template <std::size_t max_rank_ = 8>
654
+ bool all_set(tensor_view<u1x8_t, max_rank_> input) noexcept {
655
+ return popcount<max_rank_>(input).raw_ == input.numel();
656
+ }
657
+
658
+ /** @brief Population count over a 1D bit-vector view. */
659
+ inline u64_t popcount(vector_view<u1x8_t> input) noexcept { return sum<u1x8_t>(input); }
660
+
661
+ /** @brief True if any bit of the 1D bit-vector view is set. */
662
+ inline bool any_set(vector_view<u1x8_t> input) noexcept { return popcount(input).raw_ != 0; }
663
+
664
+ /** @brief True if no bit of the 1D bit-vector view is set. */
665
+ inline bool none_set(vector_view<u1x8_t> input) noexcept { return !any_set(input); }
666
+
667
+ /** @brief True if every bit of the 1D bit-vector view is set. */
668
+ inline bool all_set(vector_view<u1x8_t> input) noexcept { return popcount(input).raw_ == input.size(); }
669
+
632
670
  #pragma endregion Scalar Reductions
633
671
 
634
672
  #pragma region Axis Reductions
@@ -2,7 +2,7 @@
2
2
 
3
3
  NumKong provides single-element math operations — square root, reciprocal square root, fused multiply-add, and saturating integer arithmetic — with per-ISA implementations.
4
4
  These primitives serve as building blocks for vectorized kernels: distance finalizers call `nk_f32_rsqrt` for angular normalization, packing routines call `nk_f32_sqrt` for norm computation.
5
- Ordering functions (`nk_f16_order`, `nk_bf16_order`, `nk_e4m3_order`) convert floating-point bit patterns to integers that sort in the same order as the original floats.
5
+ Ordering functions (`nk_f16_order`, `nk_bf16_order`, `nk_e4m3_order`) compare two values through their bit patterns, returning a negative, zero, or positive `int` like `strcmp`.
6
6
 
7
7
  Reciprocal square root:
8
8
 
@@ -40,27 +40,27 @@ def saturating_add(a: int, b: int, bits: int, signed: bool) -> int:
40
40
 
41
41
  ## Input & Output Types
42
42
 
43
- | Input Type | Output Type | Description |
44
- | ---------- | ----------- | ----------------------------------------------- |
45
- | `f64` | `f64` | sqrt, rsqrt, fma for 64-bit doubles |
46
- | `f32` | `f32` | sqrt, rsqrt, fma for 32-bit floats |
47
- | `f16` | `f16` | sqrt, rsqrt, fma for 16-bit halfs |
48
- | `i8` | `i8` | Saturating add and multiply |
49
- | `u8` | `u8` | Saturating add and multiply |
50
- | `i16` | `i16` | Saturating add and multiply |
51
- | `u16` | `u16` | Saturating add and multiply |
52
- | `i32` | `i32` | Saturating add and multiply |
53
- | `u32` | `u32` | Saturating add and multiply |
54
- | `i64` | `i64` | Saturating add and multiply |
55
- | `u64` | `u64` | Saturating add and multiply |
56
- | `i4x2` | `i4x2` | Saturating add for packed signed nibble pairs |
57
- | `u4x2` | `u4x2` | Saturating add for packed unsigned nibble pairs |
58
- | `f16` | `u16` | Ordering: bit pattern to sortable integer |
59
- | `bf16` | `u16` | Ordering: bit pattern to sortable integer |
60
- | `e4m3` | `u8` | Ordering: bit pattern to sortable integer |
61
- | `e5m2` | `u8` | Ordering: bit pattern to sortable integer |
62
- | `e2m3` | `u8` | Ordering: bit pattern to sortable integer |
63
- | `e3m2` | `u8` | Ordering: bit pattern to sortable integer |
43
+ | Input Type | Output Type | Description |
44
+ | ---------- | ----------- | ------------------------------------------------------------ |
45
+ | `f64` | `f64` | sqrt, rsqrt, fma for 64-bit doubles |
46
+ | `f32` | `f32` | sqrt, rsqrt, fma for 32-bit floats |
47
+ | `f16` | `f16` | sqrt, rsqrt, fma for 16-bit halfs |
48
+ | `i8` | `i8` | Saturating add and multiply |
49
+ | `u8` | `u8` | Saturating add and multiply |
50
+ | `i16` | `i16` | Saturating add and multiply |
51
+ | `u16` | `u16` | Saturating add and multiply |
52
+ | `i32` | `i32` | Saturating add and multiply |
53
+ | `u32` | `u32` | Saturating add and multiply |
54
+ | `i64` | `i64` | Saturating add and multiply |
55
+ | `u64` | `u64` | Saturating add and multiply |
56
+ | `i4x2` | `i4x2` | Saturating add and multiply for packed signed nibble pairs |
57
+ | `u4x2` | `u4x2` | Saturating add and multiply for packed unsigned nibble pairs |
58
+ | `f16` | `int` | Ordering: three-way comparison of two values |
59
+ | `bf16` | `int` | Ordering: three-way comparison of two values |
60
+ | `e4m3` | `int` | Ordering: three-way comparison of two values |
61
+ | `e5m2` | `int` | Ordering: three-way comparison of two values |
62
+ | `e2m3` | `int` | Ordering: three-way comparison of two values |
63
+ | `e3m2` | `int` | Ordering: three-way comparison of two values |
64
64
 
65
65
  ## Optimizations
66
66
 
@@ -68,20 +68,21 @@ def saturating_add(a: int, b: int, bits: int, signed: bool) -> int:
68
68
 
69
69
  `nk_f32_rsqrt_serial` uses the classic bit-manipulation trick: reinterpret Float32 bits as Int32, compute `0x5F375A86 - (bits >> 1)`, reinterpret back to Float32, then refine with 3 Newton-Raphson iterations reaching ~34.9 correct bits.
70
70
  Each Newton-Raphson iteration: `y = y * (1.5f - 0.5f * x * y * y)` — 2 multiplies and 1 subtract, ~4cy per iteration.
71
- `nk_f32_rsqrt_haswell` replaces this with hardware `VRSQRT14PS` ($2^{-14}$ relative error, ~4cy latency) plus one Newton-Raphson refinement (~22-24 correct bits).
71
+ `nk_f32_rsqrt_haswell` replaces this with hardware `RSQRTSS` ($2^{-14}$ relative error, ~4cy latency) plus one Newton-Raphson refinement (~22-24 correct bits).
72
72
  `nk_f64_rsqrt_serial` uses the Float64 magic constant `0x5FE6EB50C7B537A9` with 4 iterations for 52-bit mantissa coverage.
73
73
 
74
74
  ### Dekker Error-Free Multiplication for FMA
75
75
 
76
- `nk_f32_fma_serial` emulates fused multiply-add on platforms without hardware FMA using Dekker's algorithm: splits each operand into high and low halves via `a_hi = (a * 134217729.0f) - ((a * 134217729.0f) - a)`, then computes the exact product error term.
77
- The magic constant $134217729 = 2^{27} + 1$ splits a 24-bit mantissa into two 12-bit halves that multiply without rounding.
76
+ `nk_f32_fma_serial` emulates fused multiply-add on platforms without hardware FMA using Dekker's algorithm: splits each operand into high and low halves via `a_high = (a * 4097.0f) - ((a * 4097.0f) - a)`, then computes the exact product error term.
77
+ The magic constant $4097 = 2^{12} + 1$ splits a 24-bit mantissa into two 12-bit halves that multiply without rounding; `nk_f64_fma_serial` uses $134217729 = 2^{27} + 1$ for the 53-bit mantissa.
78
78
  `nk_f32_fma_haswell` uses hardware `VFMADD231SS` — single instruction, single cycle, exact to the last bit.
79
79
 
80
80
  ### Float-to-Integer Ordering
81
81
 
82
- `nk_f16_order_serial`, `nk_bf16_order_serial`, `nk_e4m3_order_serial` convert floating-point bit patterns to unsigned integers that preserve the total order.
83
- Positive floats are already ordered by their bit patterns; negative floats need bit inversion: `if (bits & sign_bit) bits = ~bits; else bits ^= sign_bit`.
84
- This enables integer comparison instructions (`VPCMPUD`) for floating-point sorting without branching — used by `nk_reduce_minmax_*` for Float8 and sub-32-bit types that lack native SIMD comparison.
82
+ `nk_f16_order_serial`, `nk_bf16_order_serial`, `nk_e4m3_order_serial` map floating-point bit patterns onto signed integers that preserve the total order, then return the difference of the two keys.
83
+ Positive floats are already ordered by their bit patterns; negative floats need bit inversion, applied branchlessly as `bits ^ -sign`.
84
+ This keeps floating-point ordering on the integer pipeline — used by `nk_reduce_minmax_*` for Float8 and sub-32-bit types that lack native SIMD comparison.
85
+ `nk_f16_order_sapphire` instead uses the native `VCOMISH` half-precision comparisons.
85
86
 
86
87
  ## Performance
87
88
 
@@ -53,36 +53,39 @@ def jaccard_words(a: np.ndarray, b: np.ndarray) -> float:
53
53
 
54
54
  ## Optimizations
55
55
 
56
- ### Harley-Seal Carry-Save Adders for U1
56
+ ### Popcount Without Harley-Seal Carry-Save Adders
57
57
 
58
- `nk_hamming_u1_haswell`, `nk_jaccard_u1_haswell` amortize the cost of popcount by using Harley-Seal carry-save adder trees.
59
- Instead of computing popcount on every XOR/AND/OR result independently, three intermediate values are combined through a full-adder circuit:
58
+ The textbook way to amortize population counts is a Harley-Seal carry-save adder tree, which folds three inputs through a full-adder circuit before any popcount is issued:
60
59
 
61
60
  ```
62
61
  ones = a ^ b ^ c
63
62
  twos = (a & b) | (c & (a ^ b))
64
63
  ```
65
64
 
66
- This circuit takes three popcount inputs and produces a ones and twos accumulator, where `twos` has double the weight of `ones`.
67
- By chaining two levels, a fours accumulator is also produced, so the actual `VPSHUFB`-based popcount is called only on the final accumulated ones, twos, and fours values.
68
- The total number of popcount operations is reduced by roughly a factor of three compared to computing popcount on every vector independently.
65
+ Chaining levels yields `fours` and `eights` accumulators, each weighted by a power of two, so popcount runs on the accumulators instead of on every input vector — roughly a third of the calls.
66
+ NumKong does not do this.
67
+ The cycles-per-byte measurements in `set.h` put the crossover above 1 KB per vector: below it, loop and reduction overhead outweighs the saved popcounts, and the largest binary input benchmarked here is 4096 bits, or 512 bytes.
68
+ Ice Lake and later expose `VPOPCNTQ` outright, and on Genoa it dual-issues on ports 0-1, so the popcount pressure that CSA trees relieve is largely absent on the hardware that could run them.
69
+
70
+ AVX2 has no vector population count at all, so `nk_hamming_u1_haswell` and `nk_jaccard_u1_haswell` walk the packed octets 8 bytes at a time and call the scalar `POPCNT` through `_mm_popcnt_u64`.
71
+ `nk_hamming_u1_neon` and `nk_jaccard_u1_neon` use the NEON `CNT` instruction instead, accumulating per-byte counts in a `uint8x16_t` for at most 31 iterations before widening, since 31 x 8 still fits in a byte.
69
72
 
70
73
  ### Native VPOPCNTQ on Ice Lake
71
74
 
72
75
  `nk_hamming_u1_icelake`, `nk_jaccard_u1_icelake` use `VPOPCNTQ` on 512-bit vectors, which directly produces per-quadword population counts for 8 quadwords at once.
73
- This single instruction replaces the entire nibble-LUT + Harley-Seal pipeline used on Haswell.
74
- The kernels batch 16 vectors before horizontal reduction to minimize `VPSADBW` overhead, accumulating the per-quadword counts into a running total via `VPADDQ`.
76
+ Inputs of up to 256 bytes are fully unrolled into one, two, three, or four masked register pairs with no loop at all, since binary metrics are hard to speed up at tiny lengths.
77
+ Longer inputs enter a loop that accumulates the per-quadword counts via `VPADDQ` and reduces once at the end.
75
78
 
76
79
  ### Jaccard via Precomputed Norms
77
80
 
78
- `nk_jaccard_u1_haswell`, `nk_jaccard_u1_icelake` exploit the identity $|A \cup B| = |A| + |B| - |A \cap B|$ to avoid computing both AND-popcount and OR-popcount in the inner loop.
79
- When vector norms (popcount of each vector) are precomputed and passed via the streaming API, only the intersection popcount is needed per pair, halving the work in the critical path.
81
+ The streaming helpers `nk_jaccard_u1x64_finalize_haswell` and `nk_jaccard_u1x512_finalize_icelake` exploit the identity $|A \cup B| = |A| + |B| - |A \cap B|$ to avoid computing both AND-popcount and OR-popcount in the inner loop.
82
+ The matching `update` step accumulates only the intersection popcount; `finalize` takes the query popcount and a vector of four target popcounts and recovers the union from them, halving the work in the critical path.
83
+ Both finalizers replace the division by a reciprocal estimate — `RCPPS` on Haswell, `VRCP14PS` on Ice Lake — refined with one Newton-Raphson step.
80
84
 
81
- ### Byte Hamming via VPSADBW
85
+ ### Byte Hamming via Compare Masks
82
86
 
83
- `nk_hamming_u8_haswell`, `nk_hamming_u8_icelake` compute byte-level Hamming distance using XOR to produce per-byte difference indicators, then `VPSADBW` against zero to horizontally sum the nonzero bytes.
84
- XOR produces 0 for equal bytes and nonzero for different ones, and `VPSADBW` sums the absolute values of byte differences within each 64-bit lane.
85
- Since XOR results are either 0 or nonzero (not necessarily 1), the kernel masks XOR output through `VPMIN` with a vector of ones to clamp each byte to 0 or 1 before feeding `VPSADBW`.
87
+ `nk_hamming_u8_haswell` compares 32 bytes at a time with `VPCMPEQB`, extracts the per-byte sign bits of the two 128-bit halves with `VPMOVMSKB`, inverts the masks, and counts the set bits with scalar `POPCNT`.
88
+ `nk_hamming_u8_icelake` does the same over 64 bytes with `VPCMPNEQB`, which writes the mismatch mask straight into a `__mmask64`, so no extract step is needed.
86
89
 
87
90
  ## Performance
88
91
 
@@ -31,8 +31,7 @@
31
31
  * nk_jaccard_u1x64_init_haswell(&state_first);
32
32
  * // ... stream through packed binary vectors ...
33
33
  * nk_jaccard_u1x64_finalize_haswell(&state_first, &state_second, &state_third, &state_fourth,
34
- * query_popcount, target_popcount_a, target_popcount_b, target_popcount_c, target_popcount_d,
35
- * total_dimensions, &results);
34
+ * query_popcount, &target_popcounts_vec, total_dimensions, &result_vec);
36
35
  * @endcode
37
36
  */
38
37
  #ifndef NK_SET_HASWELL_H
@@ -240,8 +239,8 @@ NK_INTERNAL void nk_jaccard_u1x64_update_haswell(nk_jaccard_u1x64_state_haswell_
240
239
  NK_INTERNAL void nk_jaccard_u1x64_finalize_haswell( //
241
240
  nk_jaccard_u1x64_state_haswell_t const *state_a, nk_jaccard_u1x64_state_haswell_t const *state_b,
242
241
  nk_jaccard_u1x64_state_haswell_t const *state_c, nk_jaccard_u1x64_state_haswell_t const *state_d,
243
- nk_f32_t query_popcount, nk_f32_t target_popcount_a, nk_f32_t target_popcount_b, nk_f32_t target_popcount_c,
244
- nk_f32_t target_popcount_d, nk_size_t total_dimensions, nk_b128_vec_t *result) {
242
+ nk_f32_t query_popcount, nk_b128_vec_t const *target_popcounts_vec, nk_size_t total_dimensions,
243
+ nk_b128_vec_t *result_vec) {
245
244
  nk_unused_(total_dimensions);
246
245
 
247
246
  // 4-way SIMD Jaccard computation with fast reciprocal.
@@ -263,7 +262,7 @@ NK_INTERNAL void nk_jaccard_u1x64_finalize_haswell( //
263
262
  __m128 intersection_f32x4 = _mm_setr_ps(intersection_a_f32, intersection_b_f32, intersection_c_f32,
264
263
  intersection_d_f32);
265
264
  __m128 query_f32x4 = _mm_set1_ps(query_popcount);
266
- __m128 targets_f32x4 = _mm_setr_ps(target_popcount_a, target_popcount_b, target_popcount_c, target_popcount_d);
265
+ __m128 targets_f32x4 = target_popcounts_vec->xmm_ps;
267
266
  __m128 union_f32x4 = _mm_sub_ps(_mm_add_ps(query_f32x4, targets_f32x4), intersection_f32x4);
268
267
 
269
268
  // Handle zero-union edge case
@@ -283,24 +282,24 @@ NK_INTERNAL void nk_jaccard_u1x64_finalize_haswell( //
283
282
 
284
283
  __m128 ratio_f32x4 = _mm_mul_ps(intersection_f32x4, union_reciprocal_f32x4);
285
284
  __m128 jaccard_f32x4 = _mm_sub_ps(one_f32x4, ratio_f32x4);
286
- result->xmm_ps = _mm_blendv_ps(jaccard_f32x4, _mm_setzero_ps(), zero_union_mask);
285
+ result_vec->xmm_ps = _mm_blendv_ps(jaccard_f32x4, _mm_setzero_ps(), zero_union_mask);
287
286
  }
288
287
 
289
288
  /** @brief Hamming from_dot: computes pop_a + pop_b - 2*dot for 4 pairs (Haswell). */
290
- NK_INTERNAL void nk_hamming_u32x4_from_dot_haswell_(nk_b128_vec_t dots, nk_u32_t query_pop, nk_b128_vec_t target_pops,
291
- nk_b128_vec_t *results) {
292
- __m128i dots_i32x4 = dots.xmm;
289
+ NK_INTERNAL void nk_hamming_u32x4_from_dot_haswell_(nk_b128_vec_t const *dots_vec, nk_u32_t query_pop,
290
+ nk_b128_vec_t const *target_pops_vec, nk_b128_vec_t *result_vec) {
291
+ __m128i dots_i32x4 = dots_vec->xmm;
293
292
  __m128i query_i32x4 = _mm_set1_epi32((int)query_pop);
294
- __m128i target_i32x4 = target_pops.xmm;
295
- results->xmm = _mm_sub_epi32(_mm_add_epi32(query_i32x4, target_i32x4), _mm_slli_epi32(dots_i32x4, 1));
293
+ __m128i target_i32x4 = target_pops_vec->xmm;
294
+ result_vec->xmm = _mm_sub_epi32(_mm_add_epi32(query_i32x4, target_i32x4), _mm_slli_epi32(dots_i32x4, 1));
296
295
  }
297
296
 
298
297
  /** @brief Jaccard from_dot: computes 1 - dot / (pop_a + pop_b - dot) for 4 pairs (Haswell). */
299
- NK_INTERNAL void nk_jaccard_f32x4_from_dot_haswell_(nk_b128_vec_t dots, nk_u32_t query_pop, nk_b128_vec_t target_pops,
300
- nk_b128_vec_t *results) {
301
- __m128 dot_f32x4 = _mm_cvtepi32_ps(dots.xmm);
298
+ NK_INTERNAL void nk_jaccard_f32x4_from_dot_haswell_(nk_b128_vec_t const *dots_vec, nk_u32_t query_pop,
299
+ nk_b128_vec_t const *target_pops_vec, nk_b128_vec_t *result_vec) {
300
+ __m128 dot_f32x4 = _mm_cvtepi32_ps(dots_vec->xmm);
302
301
  __m128 query_f32x4 = _mm_set1_ps((nk_f32_t)query_pop);
303
- __m128 target_f32x4 = _mm_cvtepi32_ps(target_pops.xmm);
302
+ __m128 target_f32x4 = _mm_cvtepi32_ps(target_pops_vec->xmm);
304
303
  __m128 union_f32x4 = _mm_sub_ps(_mm_add_ps(query_f32x4, target_f32x4), dot_f32x4);
305
304
 
306
305
  __m128 zero_union_mask = _mm_cmpeq_ps(union_f32x4, _mm_setzero_ps());
@@ -314,7 +313,7 @@ NK_INTERNAL void nk_jaccard_f32x4_from_dot_haswell_(nk_b128_vec_t dots, nk_u32_t
314
313
 
315
314
  __m128 ratio_f32x4 = _mm_mul_ps(dot_f32x4, union_reciprocal_f32x4);
316
315
  __m128 jaccard_f32x4 = _mm_sub_ps(one_f32x4, ratio_f32x4);
317
- results->xmm_ps = _mm_blendv_ps(jaccard_f32x4, _mm_setzero_ps(), zero_union_mask);
316
+ result_vec->xmm_ps = _mm_blendv_ps(jaccard_f32x4, _mm_setzero_ps(), zero_union_mask);
318
317
  }
319
318
 
320
319
  #pragma endregion Stateful Streaming
@@ -30,8 +30,7 @@
30
30
  * nk_jaccard_u1x512_init_icelake(&state_first);
31
31
  * // ... stream through packed binary vectors ...
32
32
  * nk_jaccard_u1x512_finalize_icelake(&state_first, &state_second, &state_third, &state_fourth,
33
- * query_popcount, target_popcount_a, target_popcount_b, target_popcount_c, target_popcount_d,
34
- * total_dimensions, &results);
33
+ * query_popcount, &target_popcounts_vec, total_dimensions, &result_vec);
35
34
  * @endcode
36
35
  */
37
36
  #ifndef NK_SET_ICELAKE_H
@@ -370,8 +369,8 @@ NK_INTERNAL void nk_jaccard_u1x512_update_icelake(nk_jaccard_u1x512_state_icelak
370
369
  NK_INTERNAL void nk_jaccard_u1x512_finalize_icelake( //
371
370
  nk_jaccard_u1x512_state_icelake_t const *state_a, nk_jaccard_u1x512_state_icelake_t const *state_b,
372
371
  nk_jaccard_u1x512_state_icelake_t const *state_c, nk_jaccard_u1x512_state_icelake_t const *state_d,
373
- nk_f32_t query_popcount, nk_f32_t target_popcount_a, nk_f32_t target_popcount_b, nk_f32_t target_popcount_c,
374
- nk_f32_t target_popcount_d, nk_size_t total_dimensions, nk_b128_vec_t *result) {
372
+ nk_f32_t query_popcount, nk_b128_vec_t const *target_popcounts_vec, nk_size_t total_dimensions,
373
+ nk_b128_vec_t *result_vec) {
375
374
  nk_unused_(total_dimensions);
376
375
 
377
376
  // Port-optimized 4-way horizontal reduction using early i64 → i32 truncation.
@@ -415,7 +414,7 @@ NK_INTERNAL void nk_jaccard_u1x512_finalize_icelake( //
415
414
  // Compute Jaccard distance: 1 - intersection ÷ union
416
415
  // where union = query_popcount + target_popcount - intersection
417
416
  __m128 query_f32x4 = _mm_set1_ps(query_popcount);
418
- __m128 targets_f32x4 = _mm_setr_ps(target_popcount_a, target_popcount_b, target_popcount_c, target_popcount_d);
417
+ __m128 targets_f32x4 = target_popcounts_vec->xmm_ps;
419
418
  __m128 union_f32x4 = _mm_sub_ps(_mm_add_ps(query_f32x4, targets_f32x4), intersection_f32x4);
420
419
 
421
420
  // Handle zero-union edge case: if union == 0, result = 0.0
@@ -435,24 +434,24 @@ NK_INTERNAL void nk_jaccard_u1x512_finalize_icelake( //
435
434
 
436
435
  __m128 ratio_f32x4 = _mm_mul_ps(intersection_f32x4, union_reciprocal_f32x4);
437
436
  __m128 jaccard_f32x4 = _mm_sub_ps(one_f32x4, ratio_f32x4);
438
- result->xmm_ps = _mm_blendv_ps(jaccard_f32x4, _mm_setzero_ps(), zero_union_mask);
437
+ result_vec->xmm_ps = _mm_blendv_ps(jaccard_f32x4, _mm_setzero_ps(), zero_union_mask);
439
438
  }
440
439
 
441
440
  /** @brief Hamming from_dot: computes pop_a + pop_b - 2*dot for 4 pairs (Icelake). */
442
- NK_INTERNAL void nk_hamming_u32x4_from_dot_icelake_(nk_b128_vec_t dots, nk_u32_t query_pop, nk_b128_vec_t target_pops,
443
- nk_b128_vec_t *results) {
444
- __m128i dots_i32x4 = dots.xmm;
441
+ NK_INTERNAL void nk_hamming_u32x4_from_dot_icelake_(nk_b128_vec_t const *dots_vec, nk_u32_t query_pop,
442
+ nk_b128_vec_t const *target_pops_vec, nk_b128_vec_t *result_vec) {
443
+ __m128i dots_i32x4 = dots_vec->xmm;
445
444
  __m128i query_i32x4 = _mm_set1_epi32((int)query_pop);
446
- __m128i target_i32x4 = target_pops.xmm;
447
- results->xmm = _mm_sub_epi32(_mm_add_epi32(query_i32x4, target_i32x4), _mm_slli_epi32(dots_i32x4, 1));
445
+ __m128i target_i32x4 = target_pops_vec->xmm;
446
+ result_vec->xmm = _mm_sub_epi32(_mm_add_epi32(query_i32x4, target_i32x4), _mm_slli_epi32(dots_i32x4, 1));
448
447
  }
449
448
 
450
449
  /** @brief Jaccard from_dot: computes 1 - dot / (pop_a + pop_b - dot) for 4 pairs (Icelake). */
451
- NK_INTERNAL void nk_jaccard_f32x4_from_dot_icelake_(nk_b128_vec_t dots, nk_u32_t query_pop, nk_b128_vec_t target_pops,
452
- nk_b128_vec_t *results) {
453
- __m128 dot_f32x4 = _mm_cvtepi32_ps(dots.xmm);
450
+ NK_INTERNAL void nk_jaccard_f32x4_from_dot_icelake_(nk_b128_vec_t const *dots_vec, nk_u32_t query_pop,
451
+ nk_b128_vec_t const *target_pops_vec, nk_b128_vec_t *result_vec) {
452
+ __m128 dot_f32x4 = _mm_cvtepi32_ps(dots_vec->xmm);
454
453
  __m128 query_f32x4 = _mm_set1_ps((nk_f32_t)query_pop);
455
- __m128 target_f32x4 = _mm_cvtepi32_ps(target_pops.xmm);
454
+ __m128 target_f32x4 = _mm_cvtepi32_ps(target_pops_vec->xmm);
456
455
  __m128 union_f32x4 = _mm_sub_ps(_mm_add_ps(query_f32x4, target_f32x4), dot_f32x4);
457
456
 
458
457
  __m128 zero_union_mask = _mm_cmpeq_ps(union_f32x4, _mm_setzero_ps());
@@ -465,7 +464,7 @@ NK_INTERNAL void nk_jaccard_f32x4_from_dot_icelake_(nk_b128_vec_t dots, nk_u32_t
465
464
 
466
465
  __m128 ratio_f32x4 = _mm_mul_ps(dot_f32x4, union_reciprocal_f32x4);
467
466
  __m128 jaccard_f32x4 = _mm_sub_ps(one_f32x4, ratio_f32x4);
468
- results->xmm_ps = _mm_blendv_ps(jaccard_f32x4, _mm_setzero_ps(), zero_union_mask);
467
+ result_vec->xmm_ps = _mm_blendv_ps(jaccard_f32x4, _mm_setzero_ps(), zero_union_mask);
469
468
  }
470
469
 
471
470
  #pragma endregion Stateful Streaming
@@ -144,20 +144,22 @@ NK_PUBLIC void nk_hamming_u8_loongsonasx(nk_u8_t const *a, nk_u8_t const *b, nk_
144
144
  #pragma region Batched Finalizers
145
145
 
146
146
  /** @brief Hamming from_dot: computes pop_a + pop_b − 2 × dot for 4 pairs (LSX). */
147
- NK_INTERNAL void nk_hamming_u32x4_from_dot_loongsonasx_(nk_b128_vec_t dots, nk_u32_t query_pop,
148
- nk_b128_vec_t target_pops, nk_b128_vec_t *results) {
149
- __m128i dots_u32x4 = dots.xmm;
147
+ NK_INTERNAL void nk_hamming_u32x4_from_dot_loongsonasx_(nk_b128_vec_t const *dots_vec, nk_u32_t query_pop,
148
+ nk_b128_vec_t const *target_pops_vec,
149
+ nk_b128_vec_t *result_vec) {
150
+ __m128i dots_u32x4 = dots_vec->xmm;
150
151
  __m128i query_u32x4 = __lsx_vreplgr2vr_w((int)query_pop);
151
- __m128i target_u32x4 = target_pops.xmm;
152
- results->xmm = __lsx_vsub_w(__lsx_vadd_w(query_u32x4, target_u32x4), __lsx_vslli_w(dots_u32x4, 1));
152
+ __m128i target_u32x4 = target_pops_vec->xmm;
153
+ result_vec->xmm = __lsx_vsub_w(__lsx_vadd_w(query_u32x4, target_u32x4), __lsx_vslli_w(dots_u32x4, 1));
153
154
  }
154
155
 
155
156
  /** @brief Jaccard from_dot: computes 1 − dot / (pop_a + pop_b − dot) for 4 pairs (LSX). */
156
- NK_INTERNAL void nk_jaccard_f32x4_from_dot_loongsonasx_(nk_b128_vec_t dots, nk_u32_t query_pop,
157
- nk_b128_vec_t target_pops, nk_b128_vec_t *results) {
158
- __m128 dot_f32x4 = __lsx_vffint_s_wu(dots.xmm);
157
+ NK_INTERNAL void nk_jaccard_f32x4_from_dot_loongsonasx_(nk_b128_vec_t const *dots_vec, nk_u32_t query_pop,
158
+ nk_b128_vec_t const *target_pops_vec,
159
+ nk_b128_vec_t *result_vec) {
160
+ __m128 dot_f32x4 = __lsx_vffint_s_wu(dots_vec->xmm);
159
161
  __m128 query_f32x4 = nk_xvreplgr2vr_s_128_((nk_f32_t)query_pop);
160
- __m128 target_f32x4 = __lsx_vffint_s_wu(target_pops.xmm);
162
+ __m128 target_f32x4 = __lsx_vffint_s_wu(target_pops_vec->xmm);
161
163
  __m128 union_f32x4 = __lsx_vfsub_s(__lsx_vfadd_s(query_f32x4, target_f32x4), dot_f32x4);
162
164
 
163
165
  __m128 zero_f32x4 = (__m128)__lsx_vreplgr2vr_w(0);
@@ -167,7 +169,7 @@ NK_INTERNAL void nk_jaccard_f32x4_from_dot_loongsonasx_(nk_b128_vec_t dots, nk_u
167
169
 
168
170
  __m128 ratio_f32x4 = __lsx_vfdiv_s(dot_f32x4, safe_union_f32x4);
169
171
  __m128 jaccard_f32x4 = __lsx_vfsub_s(one_f32x4, ratio_f32x4);
170
- results->xmm_ps = (__m128)__lsx_vbitsel_v((__m128i)jaccard_f32x4, (__m128i)zero_f32x4, zero_union_mask_u32x4);
172
+ result_vec->xmm_ps = (__m128)__lsx_vbitsel_v((__m128i)jaccard_f32x4, (__m128i)zero_f32x4, zero_union_mask_u32x4);
171
173
  }
172
174
 
173
175
  #pragma endregion Batched Finalizers
@@ -34,8 +34,7 @@
34
34
  * nk_jaccard_u1x128_init_neon(&state_first);
35
35
  * // ... stream through packed binary vectors ...
36
36
  * nk_jaccard_u1x128_finalize_neon(&state_first, &state_second, &state_third, &state_fourth,
37
- * query_popcount, target_popcount_a, target_popcount_b, target_popcount_c, target_popcount_d,
38
- * total_dimensions, &results);
37
+ * query_popcount, &target_popcounts_vec, total_dimensions, &result_vec);
39
38
  * @endcode
40
39
  */
41
40
  #ifndef NK_SET_NEON_H
@@ -274,8 +273,8 @@ NK_INTERNAL void nk_jaccard_u1x128_update_neon(nk_jaccard_u1x128_state_neon_t *s
274
273
  NK_INTERNAL void nk_jaccard_u1x128_finalize_neon( //
275
274
  nk_jaccard_u1x128_state_neon_t const *state_a, nk_jaccard_u1x128_state_neon_t const *state_b,
276
275
  nk_jaccard_u1x128_state_neon_t const *state_c, nk_jaccard_u1x128_state_neon_t const *state_d,
277
- nk_f32_t query_popcount, nk_f32_t target_popcount_a, nk_f32_t target_popcount_b, nk_f32_t target_popcount_c,
278
- nk_f32_t target_popcount_d, nk_size_t total_dimensions, nk_b128_vec_t *result) {
276
+ nk_f32_t query_popcount, nk_b128_vec_t const *target_popcounts_vec, nk_size_t total_dimensions,
277
+ nk_b128_vec_t *result_vec) {
279
278
  nk_unused_(total_dimensions);
280
279
 
281
280
  // Horizontal sum using pairwise adds instead of `vaddvq_u32` (ADDV).
@@ -290,11 +289,8 @@ NK_INTERNAL void nk_jaccard_u1x128_finalize_neon( //
290
289
  float32x4_t intersection_f32x4 = vcvtq_f32_u32(intersection_u32x4);
291
290
 
292
291
  // Compute union using |A ∪ B| = |A| + |B| - |A ∩ B|
293
- // Build target popcounts vector from two independent halves (avoids serial lane insertion chain).
294
292
  float32x4_t query_f32x4 = vdupq_n_f32(query_popcount);
295
- float32x2_t targets_ab_f32x2 = vset_lane_f32(target_popcount_b, vdup_n_f32(target_popcount_a), 1);
296
- float32x2_t targets_cd_f32x2 = vset_lane_f32(target_popcount_d, vdup_n_f32(target_popcount_c), 1);
297
- float32x4_t targets_f32x4 = vcombine_f32(targets_ab_f32x2, targets_cd_f32x2);
293
+ float32x4_t targets_f32x4 = target_popcounts_vec->f32x4;
298
294
  float32x4_t union_f32x4 = vsubq_f32(vaddq_f32(query_f32x4, targets_f32x4), intersection_f32x4);
299
295
 
300
296
  // Handle zero-union edge case (empty vectors → distance = 0.0, matching scipy convention)
@@ -314,24 +310,24 @@ NK_INTERNAL void nk_jaccard_u1x128_finalize_neon( //
314
310
  // Compute Jaccard distance = 1 - intersection ÷ union
315
311
  float32x4_t ratio_f32x4 = vmulq_f32(intersection_f32x4, union_reciprocal_f32x4);
316
312
  float32x4_t jaccard_f32x4 = vsubq_f32(one_f32x4, ratio_f32x4);
317
- result->f32x4 = vbslq_f32(zero_union_mask, vdupq_n_f32(0.0f), jaccard_f32x4);
313
+ result_vec->f32x4 = vbslq_f32(zero_union_mask, vdupq_n_f32(0.0f), jaccard_f32x4);
318
314
  }
319
315
 
320
316
  /** @brief Hamming from_dot: computes pop_a + pop_b - 2*dot for 4 pairs (NEON). */
321
- NK_INTERNAL void nk_hamming_u32x4_from_dot_neon_(nk_b128_vec_t dots, nk_u32_t query_pop, nk_b128_vec_t target_pops,
322
- nk_b128_vec_t *results) {
323
- uint32x4_t dots_u32x4 = dots.u32x4;
317
+ NK_INTERNAL void nk_hamming_u32x4_from_dot_neon_(nk_b128_vec_t const *dots_vec, nk_u32_t query_pop,
318
+ nk_b128_vec_t const *target_pops_vec, nk_b128_vec_t *result_vec) {
319
+ uint32x4_t dots_u32x4 = dots_vec->u32x4;
324
320
  uint32x4_t query_u32x4 = vdupq_n_u32(query_pop);
325
- uint32x4_t target_u32x4 = target_pops.u32x4;
326
- results->u32x4 = vsubq_u32(vaddq_u32(query_u32x4, target_u32x4), vshlq_n_u32(dots_u32x4, 1));
321
+ uint32x4_t target_u32x4 = target_pops_vec->u32x4;
322
+ result_vec->u32x4 = vsubq_u32(vaddq_u32(query_u32x4, target_u32x4), vshlq_n_u32(dots_u32x4, 1));
327
323
  }
328
324
 
329
325
  /** @brief Jaccard from_dot: computes 1 - dot / (pop_a + pop_b - dot) for 4 pairs (NEON). */
330
- NK_INTERNAL void nk_jaccard_f32x4_from_dot_neon_(nk_b128_vec_t dots, nk_u32_t query_pop, nk_b128_vec_t target_pops,
331
- nk_b128_vec_t *results) {
332
- float32x4_t dot_f32x4 = vcvtq_f32_u32(dots.u32x4);
326
+ NK_INTERNAL void nk_jaccard_f32x4_from_dot_neon_(nk_b128_vec_t const *dots_vec, nk_u32_t query_pop,
327
+ nk_b128_vec_t const *target_pops_vec, nk_b128_vec_t *result_vec) {
328
+ float32x4_t dot_f32x4 = vcvtq_f32_u32(dots_vec->u32x4);
333
329
  float32x4_t query_f32x4 = vdupq_n_f32((nk_f32_t)query_pop);
334
- float32x4_t target_f32x4 = vcvtq_f32_u32(target_pops.u32x4);
330
+ float32x4_t target_f32x4 = vcvtq_f32_u32(target_pops_vec->u32x4);
335
331
  float32x4_t union_f32x4 = vsubq_f32(vaddq_f32(query_f32x4, target_f32x4), dot_f32x4);
336
332
 
337
333
  float32x4_t one_f32x4 = vdupq_n_f32(1.0f);
@@ -343,7 +339,7 @@ NK_INTERNAL void nk_jaccard_f32x4_from_dot_neon_(nk_b128_vec_t dots, nk_u32_t qu
343
339
 
344
340
  float32x4_t ratio_f32x4 = vmulq_f32(dot_f32x4, union_reciprocal_f32x4);
345
341
  float32x4_t jaccard_f32x4 = vsubq_f32(one_f32x4, ratio_f32x4);
346
- results->f32x4 = vbslq_f32(zero_union_mask, vdupq_n_f32(0.0f), jaccard_f32x4);
342
+ result_vec->f32x4 = vbslq_f32(zero_union_mask, vdupq_n_f32(0.0f), jaccard_f32x4);
347
343
  }
348
344
 
349
345
  #pragma endregion Stateful Streaming
@@ -33,8 +33,7 @@
33
33
  * nk_jaccard_u1x128_init_powervsx(&state_first);
34
34
  * // ... stream through packed binary vectors ...
35
35
  * nk_jaccard_u1x128_finalize_powervsx(&state_first, &state_second, &state_third, &state_fourth,
36
- * query_popcount, target_popcount_a, target_popcount_b, target_popcount_c, target_popcount_d,
37
- * total_dimensions, &results);
36
+ * query_popcount, &target_popcounts_vec, total_dimensions, &result_vec);
38
37
  * @endcode
39
38
  */
40
39
  #ifndef NK_SET_POWERVSX_H
@@ -224,8 +223,8 @@ NK_INTERNAL void nk_jaccard_u1x128_update_powervsx(nk_jaccard_u1x128_state_power
224
223
  NK_INTERNAL void nk_jaccard_u1x128_finalize_powervsx( //
225
224
  nk_jaccard_u1x128_state_powervsx_t const *state_a, nk_jaccard_u1x128_state_powervsx_t const *state_b,
226
225
  nk_jaccard_u1x128_state_powervsx_t const *state_c, nk_jaccard_u1x128_state_powervsx_t const *state_d,
227
- nk_f32_t query_popcount, nk_f32_t target_popcount_a, nk_f32_t target_popcount_b, nk_f32_t target_popcount_c,
228
- nk_f32_t target_popcount_d, nk_size_t total_dimensions, nk_b128_vec_t *result) {
226
+ nk_f32_t query_popcount, nk_b128_vec_t const *target_popcounts_vec, nk_size_t total_dimensions,
227
+ nk_b128_vec_t *result_vec) {
229
228
  nk_unused_(total_dimensions);
230
229
 
231
230
  // Transpose-based 4-way horizontal sum of u32x4 intersection counts
@@ -247,12 +246,7 @@ NK_INTERNAL void nk_jaccard_u1x128_finalize_powervsx( //
247
246
  vec_add(sum_lane2_u32x4, sum_lane3_u32x4));
248
247
  nk_vf32x4_t intersection_f32x4 = vec_ctf(intersection_u32x4, 0);
249
248
 
250
- // Build target popcounts vector via vec_insert
251
- nk_vf32x4_t targets_f32x4 = vec_splats(0.0f);
252
- targets_f32x4 = vec_insert(target_popcount_a, targets_f32x4, 0);
253
- targets_f32x4 = vec_insert(target_popcount_b, targets_f32x4, 1);
254
- targets_f32x4 = vec_insert(target_popcount_c, targets_f32x4, 2);
255
- targets_f32x4 = vec_insert(target_popcount_d, targets_f32x4, 3);
249
+ nk_vf32x4_t targets_f32x4 = target_popcounts_vec->vf32x4;
256
250
  nk_vf32x4_t query_f32x4 = vec_splats(query_popcount);
257
251
 
258
252
  // Compute union using |A union B| = |A| + |B| - |A intersection B|
@@ -274,25 +268,25 @@ NK_INTERNAL void nk_jaccard_u1x128_finalize_powervsx( //
274
268
  // Compute Jaccard distance = 1 - intersection / union
275
269
  nk_vf32x4_t ratio_f32x4 = vec_mul(intersection_f32x4, union_reciprocal_f32x4);
276
270
  nk_vf32x4_t jaccard_f32x4 = vec_sub(one_f32x4, ratio_f32x4);
277
- result->vf32x4 = vec_sel(jaccard_f32x4, zero_f32x4, zero_union_mask_u32x4);
271
+ result_vec->vf32x4 = vec_sel(jaccard_f32x4, zero_f32x4, zero_union_mask_u32x4);
278
272
  }
279
273
 
280
274
  /** @brief Hamming from_dot: computes pop_a + pop_b - 2 × dot for 4 pairs (Power VSX). */
281
- NK_INTERNAL void nk_hamming_u32x4_from_dot_powervsx_(nk_b128_vec_t dots, nk_u32_t query_pop, nk_b128_vec_t target_pops,
282
- nk_b128_vec_t *results) {
283
- nk_vu32x4_t dots_u32x4 = dots.vu32x4;
275
+ NK_INTERNAL void nk_hamming_u32x4_from_dot_powervsx_(nk_b128_vec_t const *dots_vec, nk_u32_t query_pop,
276
+ nk_b128_vec_t const *target_pops_vec, nk_b128_vec_t *result_vec) {
277
+ nk_vu32x4_t dots_u32x4 = dots_vec->vu32x4;
284
278
  nk_vu32x4_t query_u32x4 = vec_splats(query_pop);
285
- nk_vu32x4_t target_u32x4 = target_pops.vu32x4;
279
+ nk_vu32x4_t target_u32x4 = target_pops_vec->vu32x4;
286
280
  nk_vu32x4_t two_dots_u32x4 = vec_add(dots_u32x4, dots_u32x4);
287
- results->vu32x4 = vec_sub(vec_add(query_u32x4, target_u32x4), two_dots_u32x4);
281
+ result_vec->vu32x4 = vec_sub(vec_add(query_u32x4, target_u32x4), two_dots_u32x4);
288
282
  }
289
283
 
290
284
  /** @brief Jaccard from_dot: computes 1 - dot / (pop_a + pop_b - dot) for 4 pairs (Power VSX). */
291
- NK_INTERNAL void nk_jaccard_f32x4_from_dot_powervsx_(nk_b128_vec_t dots, nk_u32_t query_pop, nk_b128_vec_t target_pops,
292
- nk_b128_vec_t *results) {
293
- nk_vf32x4_t dot_f32x4 = vec_ctf(dots.vu32x4, 0);
285
+ NK_INTERNAL void nk_jaccard_f32x4_from_dot_powervsx_(nk_b128_vec_t const *dots_vec, nk_u32_t query_pop,
286
+ nk_b128_vec_t const *target_pops_vec, nk_b128_vec_t *result_vec) {
287
+ nk_vf32x4_t dot_f32x4 = vec_ctf(dots_vec->vu32x4, 0);
294
288
  nk_vf32x4_t query_f32x4 = vec_splats((nk_f32_t)query_pop);
295
- nk_vf32x4_t target_f32x4 = vec_ctf(target_pops.vu32x4, 0);
289
+ nk_vf32x4_t target_f32x4 = vec_ctf(target_pops_vec->vu32x4, 0);
296
290
  nk_vf32x4_t union_f32x4 = vec_sub(vec_add(query_f32x4, target_f32x4), dot_f32x4);
297
291
 
298
292
  nk_vf32x4_t one_f32x4 = vec_splats(1.0f);
@@ -308,7 +302,7 @@ NK_INTERNAL void nk_jaccard_f32x4_from_dot_powervsx_(nk_b128_vec_t dots, nk_u32_
308
302
 
309
303
  nk_vf32x4_t ratio_f32x4 = vec_mul(dot_f32x4, union_reciprocal_f32x4);
310
304
  nk_vf32x4_t jaccard_f32x4 = vec_sub(one_f32x4, ratio_f32x4);
311
- results->vf32x4 = vec_sel(jaccard_f32x4, zero_f32x4, zero_union_mask_u32x4);
305
+ result_vec->vf32x4 = vec_sel(jaccard_f32x4, zero_f32x4, zero_union_mask_u32x4);
312
306
  }
313
307
 
314
308
  #if defined(__clang__)