RFR: 8355563: VectorAPI: Refactor current implementation of subword gather load API
Xiaohong Gong
xgong at openjdk.org
Tue May 20 02:24:51 UTC 2025
On Mon, 19 May 2025 03:10:46 GMT, Xiaohong Gong <xgong at openjdk.org> wrote:
>> JDK-8318650 introduced hotspot intrinsification of subword gather load APIs for X86 platforms [1]. However, the current implementation is not optimal for AArch64 SVE platform, which natively supports vector instructions for subword gather load operations using an int vector for indices (see [2][3]).
>>
>> Two key areas require improvement:
>> 1. At the Java level, vector indices generated for range validation could be reused for the subsequent gather load operation on architectures with native vector instructions like AArch64 SVE. However, the current implementation prevents compiler reuse of these index vectors due to divergent control flow, potentially impacting performance.
>> 2. At the compiler IR level, the additional `offset` input for `LoadVectorGather`/`LoadVectorGatherMasked` with subword types increases IR complexity and complicates backend implementation. Furthermore, generating `add` instructions before each memory access negatively impacts performance.
>>
>> This patch refactors the implementation at both the Java level and compiler mid-end to improve efficiency and maintainability across different architectures.
>>
>> Main changes:
>> 1. Java-side API refactoring:
>> - Explicitly passes generated index vectors to hotspot, eliminating duplicate index vectors for gather load instructions on
>> architectures like AArch64.
>> 2. C2 compiler IR refactoring:
>> - Refactors `LoadVectorGather`/`LoadVectorGatherMasked` IR for subword types by removing the memory offset input and incorporating it into the memory base `addr` at the IR level. This simplifies backend implementation, reduces add operations, and unifies the IR across all types.
>> 3. Backend changes:
>> - Streamlines X86 implementation of subword gather operations following the removal of the offset input from the IR level.
>>
>> Performance:
>> The performance of the relative JMH improves up to 27% on a X86 AVX512 system. Please see the data below:
>>
>> Benchmark Mode Cnt Unit SIZE Before After Gain
>> GatherOperationsBenchmark.microByteGather128 thrpt 30 ops/ms 64 53682.012 52650.325 0.98
>> GatherOperationsBenchmark.microByteGather128 thrpt 30 ops/ms 256 14484.252 14255.156 0.98
>> GatherOperationsBenchmark.microByteGather128 thrpt 30 ops/ms 1024 3664.900 3595.615 0.98
>> GatherOperationsBenchmark.microByteGather128 thrpt 30 ops/ms 4096 908.31...
>
> Ping again~ could any one please take a look at this PR? Thanks a lot!
> Hi @XiaohongGong , Very nice work!, Looks good to me, will do some testing and get back.
>
> Do you have any idea about following regression?
>
> ```
> GatherOperationsBenchmark.microByteGather256 thrpt 30 ops/ms 64 55844.814 48311.847 0.86
> GatherOperationsBenchmark.microByteGather256 thrpt 30 ops/ms 256 15139.459 13009.848 0.85
> GatherOperationsBenchmark.microByteGather256 thrpt 30 ops/ms 1024 3861.834 3284.944 0.85
> GatherOperationsBenchmark.microByteGather256 thrpt 30 ops/ms 4096 938.665 817.673 0.87
> ```
>
> Best Regards
Yes, I also observed such regression. After analyzing, I found it was caused by the java side changes, which influences the range check elimination inside `IntVector.fromArray()` and `ByteVector.intoArray()` in the benchmark. The root cause is the counted loop in following benchmark case is not recognized by compiler as expected:
public void microByteGather256() {
for (int i = 0; i < SIZE; i += B256.length()) {
ByteVector.fromArray(B256, barr, 0, index, i)
.intoArray(bres, i);
}
}
```
The loop iv phi node is not recognized successfully when C2 recognize the counted loop pattern, because it was casted twice with `CastII` in this case. The ideal graph looks like:
Loop
\
\ / -------------------------
\ / |
Phi |
| |
CastII |
| |
CastII |
| |
\ ConI |
\ | |
AddVI |
|--------------------|
Relative code is https://github.com/openjdk/jdk/blob/master/src/hotspot/share/opto/loopnode.cpp#L1667.
Before the change, the graph should be:
Loop
\
\ / -------------------------
\ / |
Phi |
| |
CastII |
| | |
\ ConI |
\ | |
AddVI |
|--------------------|
```
The difference comes from the index generation in `ByteVector.fromArray()` (I mean calling of `IntVector.fromArray()` in java). Before, the `i` in above loop is not directly used by `IntVector.fromArray()`. Instead, it was used by another loop phi node. Hence, there is no additional `CastII`. But after my change, the loop in the java implementation of `ByteVector.fromArray()` is removed, and `i` is directly used by `IntVector.fromArray()` . It will be used by boundary check before loading the indexes. Hence another `CastII` is generated.
When recognizing the loop iv phi node, it will check whether the `Phi` is used by a `CastII` first. And get the input of the `CastII` if then. But this check only happens once. See https://github.com/openjdk/jdk/blob/master/src/hotspot/share/opto/loopnode.cpp#L1659
if (xphi->Opcode() == Op_Cast(iv_bt)) {
xphi = xphi->in(1);
}
Once the `PhiNode` is casted more than one times, the pattern is not recognized. I think we should refine the logic of recognizing the counted loop, by changing above `if` to `while` to make the iv phi node is recognized successfully. Potential change should be:
while (xphi->Opcode() == Op_Cast(iv_bt)) {
xphi = xphi->in(1);
}
I'v tested this change, and found the benchmarks with regression can be improved as before. Consider I'm not familiar with C2's loop transform code, I prefer to do more investigation for this issue, and may fix it with a followed-up patch. Any suggestions?
-------------
PR Comment: https://git.openjdk.org/jdk/pull/25138#issuecomment-2892720654
More information about the core-libs-dev
mailing list