LantaoJin opened a new pull request, #16807:
URL: https://github.com/apache/lucene/pull/16807

   ### Description
   Since #15148, `PanamaVectorUtilSupport` computes `dotProduct(byte[], 
byte[])` and `uint8DotProduct` with the same methods, and `squareDistance` and 
`uint8SquareDistance` too. A `boolean signed` is passed down to the 
128/256/512-bit loops, which choose their widening conversion with `signed ? 
B2S : ZERO_EXTEND_B2S` (and the same for `S2I` and `B2I`). This is fine while a 
JVM uses only one of the two kernels. Once it has used both, C2 compiles the 
shared methods for both values of the flag, and the kernel that was used second 
stays slow for the life of the JVM:
   
   - On aarch64 (NEON, 128-bit) the conversion operator is no longer a constant 
in the compiled loop, so the Vector API conversion is not intrinsified 
(`-XX:+PrintIntrinsics` shows `missing constant ... etype_to=Phi` and 
`VectorSupport::convert ... failed to inline (intrinsic)`) and runs the Java 
fallback. The second kernel is **65x to 150x slower**.
   - On x86 with 512-bit vectors the conversions are still intrinsified, but 
the shared loop is recompiled out of line and its callers no longer inline it 
(`already compiled into a big method`). The second kernel is **4x to 5x 
slower**. 
   
   This is easy to hit. `Lucene104ScalarQuantizedVectorScorer` scores 
`UNSIGNED_BYTE` (8-bit) fields with `uint8DotProduct` and `SEVEN_BIT` fields 
with `dotProduct`, so a JVM that searches both kinds of field mixes the two 
kernels; `KnnByteVectorField` uses the signed ones. JMH benchmarks do not show 
it, because JMH forks a new JVM for each benchmark.
   
   This PR gives the two kernels separate methods all the way down. 
`dotProductBody`, `squareDistanceBody` and their per-width loops are again 
exactly the code from before #15148. The unsigned kernels get their own 
`uint8DotProductBody` and `uint8SquareDistanceBody` with their own copies of 
the loops, which use the `ZERO_EXTEND_*` conversions. Each public method calls 
one or the other.
   
   **Reproduction**, with `--add-modules jdk.incubator.vector`:
   
   ```java
   byte[] a = new byte[768], b = new byte[768];
   new Random(42).nextBytes(a);
   new Random(43).nextBytes(b);
   long sink = 0;
   for (int i = 0; i < 2_000_000; i++) sink += VectorUtil.uint8DotProduct(a, b);
   for (int round = 0; round < 10; round++) {
     long start = System.nanoTime();
     for (int i = 0; i < 100_000; i++) sink += VectorUtil.dotProduct(a, b);
     System.out.printf("dotProduct: %.1f ns/op%n", (System.nanoTime() - start) 
/ 100_000.0);
   }
   ```
   On aarch64 this prints about 21,300 ns/op in every round before this change, 
and 339 ns/op after.
   
   **Benchmark,** 768-dimension vectors, ns per call of the second kernel once 
it is steady, JDK 25. The x86 machine is a Xeon Platinum 8175M; it uses 256-bit 
vectors by default, so the 512-bit rows use `-XX:UseAVX=3`.
   
   | machine | first kernel, then second kernel | first kernel alone | second, 
before | second, after |
   |---|---|---|---|---|
   | aarch64 (Neoverse-N1) | `uint8DotProduct`, then `dotProduct` | 332-343 | 
21,468 | 343 |
   | aarch64 (Neoverse-N1) | `dotProduct`, then `uint8DotProduct` | 334-344 | 
50,087 | 342 |
   | x86, 512-bit | `uint8DotProduct`, then `dotProduct` | 59-65 | 271-272 | 
59-65 |
   | x86, 512-bit | `dotProduct`, then `uint8DotProduct` | 59-61 | 327-328 | 
61-64 |
   | x86, 512-bit | `uint8SquareDistance`, then `squareDistance` | 73-80 | 
217-224 | 73-80 |
   | x86, 512-bit | `squareDistance`, then `uint8SquareDistance` | 74-80 | 
258-262 | 73-81 |
   
   With 256-bit vectors on x86, the second kernel runs at 109-122 ns before and 
after this change. On aarch64, square distance did not slow down in my runs, 
only because its loop happens to be inlined into a caller where the flag is a 
constant; when the loop is kept out of line with 
`-XX:CompileCommand=dontinline`, `squareDistance` followed by 
`uint8SquareDistance` is 981 ns per call before and 441 ns after (418-427 ns 
alone).
   
   End to end on aarch64, I used one index with an `UNSIGNED_BYTE` field and a 
`SEVEN_BIT` field (`Lucene104HnswScalarQuantizedVectorsFormat`, 10,000 docs, 
384 dimensions, force-merged, k = 10). Querying each field alone in its own JVM 
takes 236 and 237 µs per query. Querying both in one JVM takes 9,208 and 5,166 
µs before this change, and 214 and 212 µs after. On x86 with 512-bit vectors, 
the end-to-end effect is small (about 1.1x to 1.5x) and noisy, and I could not 
see it after this change.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to