RFR: 8346964: C2: Improve integer multiplication with constant in MulINode::Ideal()

erifan duke at openjdk.org
Mon Feb 10 21:25:01 UTC 2025


On Tue, 7 Jan 2025 17:30:04 GMT, Emanuel Peter <epeter at openjdk.org> wrote:

>> Constant multiplication x*C can be optimized as LEFT SHIFT, ADD or SUB instructions since generally these instructions have smaller latency and larger throughput on most architectures. For example:
>> 1. x*8 can be optimized as x<<3.
>> 2. x*9 can be optimized as x+x<<3, and x+x<<3 can be lowered as one SHIFT-ADD (ADD instruction combined with LEFT SHIFT) instruction on some architectures, like aarch64 and x86_64.
>> 
>> Currently OpenJDK implemented a few such patterns in mid-end, including:
>> 1. |C| = 1<<n                (n>0)
>> 2. |C| = (1<<n) - 1          (n>0)
>> 3. |C| = (1<<m) + (1<<n)     (m>n, n>=0)
>> 
>> The first two are ok. Because on most architectures they are lowered as only one ADD/SUB/SHIFT instruction.
>> 
>> But the third pattern doesn't always perform well on some architectures, such as aarch64. The third pattern can be split as the following sub patterns:
>> 3.1. C = (1<<n) + 1          (n>0)
>> 3.2. C = -((1<<n) + 1)       (n>0)
>> 3.3. C = (1<<m) + (1<<n)     (m>n, n>0)
>> 3.4. C = -((1<<m) + (1<<n))  (m>n, n>0)
>> 
>> According to Arm optimization guide, if the shift amount > 4, the latency and throughput of ADD instruction is the same with MUL instruction. So in this case, converting MUL to ADD is not profitable. Take a[i] * C on aarch64 as an example.
>> 
>> Before (MUL is not converted):
>> 
>>   mov x1, #C
>>   mul x2, x1, x0
>> 
>> 
>> Now (MUL is converted):
>>   For 3.1:
>> 
>>     add x2, x0, x0, lsl #n
>> 
>> 
>>   For 3.2:
>> 
>>     add x2, x0, x0, lsl #n  // same cost with mul if n > 4
>>     neg x2, x2
>> 
>> 
>>   For 3.3:
>> 
>>     lsl x1, x0, #m
>>     add x2, x1, x0, lsl #n  // same cost with mul if n > 4
>> 
>> 
>>   For 3.4:
>> 
>>     lsl x1, x0, #m
>>     add x2, x1, x0, lsl #n  // same cost with mul if n > 4
>>     neg x2, x2
>> 
>> 
>> Test results (ns/op) on Arm Neoverse V2:
>> 
>> 		Before	Now	Uplift		Pattern	Notes
>> testInt9	103.379	60.702	1.70305756	3.1
>> testIntN33	103.231	106.825	0.96635619	3.2	n > 4
>> testIntN9	103.448	103.005	1.004300762	3.2	n <= 4
>> testInt18	103.354	99.271	1.041129837	3.3	m <= 4, n <= 4
>> testInt36	103.396	99.186	1.042445506	3.3	m > 4, n <= 4
>> testInt96	103.337	105.416	0.980278136	3.3	m > 4, n > 4
>> testIntN18	103.333	139.258	0.742025593	3.4	m <= 4, n <= 4
>> testIntN36	103.208	139.132	0.741799155	3.4	m > 4, n <= 4
>> testIntN96	103.367	139.471	0.74113615	3.4	m > 4, n > 4
>> 
>> 
>> **(S1) From this point on, we should treat pattern 3 as follows:**
>> 3.1  C = (1<<n) + 1          (n>0)
>> 3.2  C = -((1<<n) + 1)       (0<n<=4)
>> 3.3  C...
>
> Hi @erifan
> 
> I have some first questions / comments. I only scanned through quickly.
> 
> My biggest question:
> You only mention aarch64. But we would need to know that your changes also work well on x64.
> 
> Also:
> Can you summarize if your changes are only for performane of vectorization, or also for scalar code?

Hi @eme64 , thanks for your review!

> My biggest question: You only mention aarch64. But we would need to know that your changes also work well on x64.

Yes, this patch also benefits x86_64 platform. I tested the patch on aarch64 V2 and N1 processors, AMD64 Genoa and Intel SPR processors (I can provide the test results if necessary). The test results show that for some cases the performance uplift is very large and there is no obvious performance degradation. I'm not familiar with x86_64 instruction set, so I didn't do any theoretical analysis on x86_64 platform, I did very detailed theoretical analysis on aarch64 platform.

I don't have machines with architectures other than aarch64 and x64, so this patch is not tested on platforms except for aarch64 and x64.

> Also: Can you summarize if your changes are only for performane of vectorization, or also for scalar code?

For both vectorization and scalar cases. For example the pattern x * C => -((x<<m) + (x<<n)) is supported now, this pattern is a negative optimization from both the vectorization and scalar perspectives on both aarch64 and x64, and this patch removed this pattern.

As mentioned in my commit message, this patch is good for most cases, but there are small performance loss for some cases. Ideally, if we implement vplan, I think it would be better to keep the multiplication operation before vectorization and let the vectorizer generate the optimal vector code. Then for the scalar case that cannot be vectorized, convert to other more optimal instructions in the backend ( or in [this pass](https://github.com/openjdk/jdk/pull/21599) if it's merged) if necessary.

> src/hotspot/share/opto/mulnode.cpp line 253:
> 
>> 251:   }
>> 252: 
>> 253:   // TODO: abs_con = (1<<m)+(1<<n) and con = -((1<<n)+1) was supported
> 
> Is this `TODO` here on purpose?

Yes, it's a reminder to implement these two patterns for scalar cases in backends if they are worthwhile on the specific architecture.

> src/hotspot/share/opto/mulnode.cpp line 263:
> 
>> 261:   //
>> 262:   // But if it's not vectorizable, maybe it's profitable to do the conversion on
>> 263:   // some architectures, support it in backends if it's worthwhile.
> 
> If it is all about vectorization only: we could consider delaying the `Ideal` optimization until after loop-opts. Then we can keep the multiplication for vectorization, and only use shift/add once we know that we cannot vectorize.

But for some cases, it's better to do the conversion before vectorization, for example:
`x * 8 => x << 3 `

Through test results and theoretical analysis (only on aarch64, see the commit message), I found that we'd better to do the conversion before vectorization if the multiplication can be transformed to less than one or two shift/add instructions.

> src/hotspot/share/opto/mulnode.cpp line 273:
> 
>> 271:         new LShiftINode(in(1), phase->intcon(log2i_exact(abs_con - 1))));
>> 272:     res = new AddINode(in(1), n1);
>> 273:   } else if (is_power_of_2(abs_con + 1)) {
> 
> So now you only check for `power_of_2   +- 1`, right? But before we also looked at patterns with 2 bits, such as `64 + 8`.
> 
> You would really need to prove that this is not a loss on any of the platforms we care about, incl. x64.

Yes, I have tested the patch on several of x64 machines, including amd Genoa and Intel SPR and some older x86 machines, there's no noticeable performance loss.  Test results on aarch64 and amd Genoa were included in the commit message, please take a look, thanks~

-------------

PR Comment: https://git.openjdk.org/jdk/pull/22922#issuecomment-2576610164
PR Review Comment: https://git.openjdk.org/jdk/pull/22922#discussion_r1906216214
PR Review Comment: https://git.openjdk.org/jdk/pull/22922#discussion_r1906220122
PR Review Comment: https://git.openjdk.org/jdk/pull/22922#discussion_r1906222636


More information about the hotspot-compiler-dev mailing list