RFR: 8346964: C2: Improve integer multiplication with constant in MulINode::Ideal()
Emanuel Peter
epeter at openjdk.org
Mon Feb 10 21:25:01 UTC 2025
On Mon, 6 Jan 2025 07:55:39 GMT, erifan <duke 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 = (1<<m) + (1<<n) (m>n, 0<n<=4)
> 3.4 C = -((1<<m) + (1<<n)) (disable)
>
> Since this conversion is implemented in mid-end, it impacts...
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?
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?
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.
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.
test/hotspot/jtreg/compiler/c2/TestSerialAdditions.java line 148:
> 146: @IR(counts = { IRNode.MUL_I, "1" })
> 147: private static int addTo6(int a) {
> 148: return a + a + a + a + a + a;
Is this an improvement?
test/hotspot/jtreg/compiler/loopopts/superword/TestAlignVector.java line 1059:
> 1057: b[i * 6 + 1] = (byte) (a[i * 6 + 1] & mask);
> 1058: b[i * 6 + 2] = (byte) (a[i * 6 + 2] & mask);
> 1059: b[i * 6 + 3] = (byte) (a[i * 6 + 3] & mask);
Why did this change?
Was `i * 6` not supposed to be changed to `(i << 2) + (i << 1)`? This is the reason why the current impl of VPointer is not parsing this right, but after my patch https://github.com/openjdk/jdk/pull/21926 this will be fixed because we will parse the multiple occurances of `i` properly.
So it looks like you are now sometimes keeping it at `i * 6` instead of splitting it. Why?
-------------
PR Review: https://git.openjdk.org/jdk/pull/22922#pullrequestreview-2534960901
PR Review Comment: https://git.openjdk.org/jdk/pull/22922#discussion_r1905807007
PR Review Comment: https://git.openjdk.org/jdk/pull/22922#discussion_r1905813470
PR Review Comment: https://git.openjdk.org/jdk/pull/22922#discussion_r1905816741
PR Review Comment: https://git.openjdk.org/jdk/pull/22922#discussion_r1905808320
PR Review Comment: https://git.openjdk.org/jdk/pull/22922#discussion_r1905802482
More information about the hotspot-compiler-dev
mailing list