Skip to content

8355216: Accelerate P-256 arithmetic on aarch64 - #30941

Closed
ferakocz wants to merge 12 commits into
openjdk:masterfrom
ferakocz:p256-aarch64
Closed

8355216: Accelerate P-256 arithmetic on aarch64#30941
ferakocz wants to merge 12 commits into
openjdk:masterfrom
ferakocz:p256-aarch64

Conversation

@ferakocz

@ferakocz ferakocz commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

An aarch64 implementation of the MontgomeryIntegerPolynomial256.mult() method and IntegerPolynomial.conditionalAssign(). Since 64-bit multiplication is not supported on Neon and manually performing this operation with 32-bit limbs is slower than with GPRs, a hybrid neon/gpr approach is used. Neon instructions are used to compute intermediate values used in the last two iterations of the main "loop", while the GPRs compute the first few iterations. At the method level this improves performance by ~9% and at the API level roughly 5%.



Progress

  • Change must not contain extraneous whitespace
  • Commit message must refer to an issue
  • Change must be properly reviewed (2 reviews required, with at least 1 Reviewer, 1 Author)

Issue

  • JDK-8355216: Accelerate P-256 arithmetic on aarch64 (Enhancement - P3)

Reviewers

Reviewing

Using git

Checkout this PR locally:
$ git fetch https://git.openjdk.org/jdk.git pull/30941/head:pull/30941
$ git checkout pull/30941

Update a local copy of the PR:
$ git checkout pull/30941
$ git pull https://git.openjdk.org/jdk.git pull/30941/head

Using Skara CLI tools

Checkout this PR locally:
$ git pr checkout 30941

View PR using the GUI difftool:
$ git pr show -t 30941

Using diff file

Download this PR as a diff file:
https://git.openjdk.org/jdk/pull/30941.diff

Using Webrev

Link to Webrev Comment

@bridgekeeper

bridgekeeper Bot commented Apr 27, 2026

Copy link
Copy Markdown

👋 Welcome back ferakocz! A progress list of the required criteria for merging this PR into master will be added to the body of your pull request. There are additional pull request commands available for use with this pull request.

@openjdk

openjdk Bot commented Apr 27, 2026

Copy link
Copy Markdown

@ferakocz This change now passes all automated pre-integration checks.

ℹ️ This project also has non-automated pre-integration requirements. Please see the file CONTRIBUTING.md for details.

After integration, the commit message for the final commit will be:

8355216: Accelerate P-256 arithmetic on aarch64

Reviewed-by: adinn, aph

You can use pull request commands such as /summary, /contributor and /issue to adjust it as needed.

At the time when this comment was updated there had been 11 new commits pushed to the master branch:

As there are no conflicts, your changes will automatically be rebased on top of these commits when integrating. If you prefer to avoid this automatic rebasing, please check the documentation for the /integrate command for further details.

As you do not have Committer status in this project an existing Committer must agree to sponsor your change. Possible candidates are the reviewers of this PR (@adinn, @shipilev, @theRealAph) but any other Committer may sponsor as well.

➡️ To flag this PR as ready for integration with the above commit message, type /integrate in a new comment. (Afterwards, your sponsor types /sponsor in a new comment to perform the integration).

@openjdk

openjdk Bot commented Apr 27, 2026

Copy link
Copy Markdown

@ferakocz this pull request can not be integrated into master due to one or more merge conflicts. To resolve these merge conflicts and update this pull request you can run the following commands in the local repository for your personal fork:

git checkout p256-aarch64
git fetch https://git.openjdk.org/jdk.git master
git merge FETCH_HEAD
# resolve conflicts and follow the instructions given by git merge
git commit -m "Merge master"
git push

@openjdk openjdk Bot added merge-conflict Pull request has merge conflict with target branch hotspot hotspot-dev@openjdk.org labels Apr 27, 2026
@openjdk

openjdk Bot commented Apr 27, 2026

Copy link
Copy Markdown

@ferakocz The following label will be automatically applied to this pull request:

  • hotspot

When this pull request is ready to be reviewed, an "RFR" email will be sent to the corresponding mailing list. If you would like to change these labels, use the /label pull request command.

@openjdk

openjdk Bot commented Apr 27, 2026

Copy link
Copy Markdown

The total number of required reviews for this PR has been set to 2 based on the presence of this label: hotspot. This can be overridden with the /reviewers command.

@openjdk openjdk Bot added the rfr Pull request is ready for review label Apr 27, 2026
@ferakocz ferakocz changed the title 8355216: Accelerate P-256 arithmetic on aarch64 (revived) 8355216: Accelerate P-256 arithmetic on aarch64 Apr 27, 2026
@openjdk openjdk Bot added the core-libs core-libs-dev@openjdk.org label Apr 27, 2026
@openjdk

openjdk Bot commented Apr 27, 2026

Copy link
Copy Markdown

@ferakocz core-libs has been added to this pull request based on files touched in new commit(s).

@mlbridge

mlbridge Bot commented Apr 27, 2026

Copy link
Copy Markdown

@openjdk openjdk Bot removed the merge-conflict Pull request has merge conflict with target branch label Apr 27, 2026
@eastig

eastig commented Apr 27, 2026

Copy link
Copy Markdown
Member

@ferakocz

At the method level this improves performance by ~9% and at the API level roughly 5%.

Can you provide more information about this? What hardware did you use? What benchmarks did you run?

Comment on lines +7833 to +7838
__ umulh(high, a_i, b_0);
__ mul(low, a_i, b_0);
__ lsl(high, high, shift1);
__ lsr(tmp, low, shift2);
__ orr(high, high, tmp);
__ andr(low, low, limb_mask);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a recurring pattern:

__ umulh(hi, a, b);
__ mul(lo, a, b)
__ lsl(hi, hi, SHIFT1)
__ lsr(tmp, lo, SHIFT2)
__ orr(hi, hi, tmp)
__ andr(lo, lo, mask)

which could be abstracted as a macro method (picking an arbitrary name that you probably want to improve on):

p256_partial_mul(Register a, Register b, Register hi, Register lo, Register tmp, Register mask)

You can then simplify the code that processes this limb (likewise in each each subsequent limb) to make it clearer what is being done to combine the results of these macro computations:

__ ldr(a_i, __ post(a, 8));

p256_partial_mul(a_i, b_0, high, low, tmp, limb_mask);

__ andr(n, low, limb_mask);

neon_partial_mult_64(B, b_highs, a_vals, 0);

p256_partial_mul(n, mod_0, mod_high, mod_low, tmp, limb_mask)

__ add(low, low, mod_low);
__ add(high, high, mod_high);
__ lsr(c_i, low, shift2);
__ add(c_i, c_i, high);

Also, note that the function consumes SHIFT1 and SHIFT2 which should be defined as final int constants and would be better defined at file scope rather than being declared and initialized as local variables.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very good idea! Thanks a lot!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines +7950 to +7958
__ st1(A[0], __ T2D, __ post(mul_ptr, 16));
__ st1(D[0], __ T2D, __ post(mul_ptr, 16));
__ st1(A[1], __ T2D, __ post(mul_ptr, 16));
__ st1(D[1], __ T2D, __ post(mul_ptr, 16));

__ st1(A[2], __ T2D, __ post(mul_ptr, 16));
__ st1(D[2], __ T2D, __ post(mul_ptr, 16));
__ st1(A[3], __ T2D, __ post(mul_ptr, 16));
__ st1(D[3], __ T2D, mul_ptr);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could usefully abstract this as a VSeq template function

vs_st1_interleaved(VSeq<N> A, VSeq<N> B, Register dest) {
  for (int i = 0; i < N; i++) {
    __ st1(A[i], __ T2D, __ post(dest, 16));
    __ st1(B[i], __ T2D, __ post(dest, 16));
  }
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea. Thanks!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@YouliangGuo

Copy link
Copy Markdown

@ferakocz

At the method level this improves performance by ~9% and at the API level roughly 5%.

Can you provide more information about this? What hardware did you use? What benchmarks did you run?

This is a continuation of #27946, which includes all the testing and benchmark results.

__ str(c_i, Address(c_ptr, 8));
__ mov(c_i, high);

vs_shl(D, __ T2D, D, 12);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
vs_shl(D, __ T2D, D, 12);
vs_shl(D, __ T2D, D, montMulP256Shift1);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed.

// generate_intpoly_montgomeryMult_P256().
// This function computes partial results of eight 52 x 52 bit multiplications,
// where the multiplicands are stored as 64-bit values, specifically
// (b_0, b_1, b_2, b_3) * (a_3, a_4).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this not be (a_0, a_1)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Fixed.

@adinn adinn May 18, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

n.b. I see now why you originally wrote (a_3, a_4). My confusion arose because at this point it was not clear tome that the as being input here were actually the 4th and fifth limb of a sequence of 5 52-bit quantities taken from input array a.

I still think it is clearer to keep a_0 and a_1 here. The comments I suggested you add to the calling routine make it clear that in the context of that method the pair of 52-bit values in register as are the 4th and 5th limbs (a3, a4). Meanwhile in this routine where we accept two full a values and four half b values it makes more sense to label the as as a_0 and a_1.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right. I added some more comments to explain why it is a_3 and a_4.

Comment on lines +7734 to +7738
// In a call to this function, either the high or low 32 bits of the b_i
// values are multiplied by either the high or low 32 bits of the a_j values,
// so four calls with the appropriate parameters will produce the 64-bit
// low32 * low32, low32 * high32, high32 * low32, high32 * high32
// values in the output register sequences.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A little more detail would make it easier to understand this method and helpt to clarify what is happening in code where it is called

Suggested change
// In a call to this function, either the high or low 32 bits of the b_i
// values are multiplied by either the high or low 32 bits of the a_j values,
// so four calls with the appropriate parameters will produce the 64-bit
// low32 * low32, low32 * high32, high32 * low32, high32 * high32
// values in the output register sequences.
// Calls to this function accept either the low 32 bis or high 20 bits
// of each b_i packed into bs in ascending order. a_0 and a_1 are packed
// into successive 64 bit elements of as. lane selects the low 32 or high
// 20 bits of each a_j value. So four calls with the appropriate parameters
// will produce the 64-bit low32 * low32, low32 * high20, high20 * low32,
// high20 * high20 values in the output register sequences vs. The
// 64-bit partial products are returned in vs in ascending order:
// vs[0] = (b_0*a_0, b_1*a_0) . . . vs[3] = (b_2*a_1, b_3*a_1)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accepted with minor changes.

__ sub(sp, sp, 48);
__ mov(c_ptr, sp);

// Calculate limb mask

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Calculate limb mask
// Calculate (52-bit) limb masks for both gpr and vector registers

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accepted.

__ ldr(mod_3, __ post(mod_ptr, 8));
__ ldr(mod_4, mod_ptr);
__ ld1(a_vals, __ T2D, a_ptr);
__ ld2(b_lows, b_highs, __ T4S, b);

@adinn adinn May 18, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
__ ld2(b_lows, b_highs, __ T4S, b);
// use an interleaved load to group low 32 bits and high 20 bits
// of 4 successive b values into two vector registers
// n.b. these are the same inputs as the ones in b_0 ... b4
__ ld2(b_lows, b_highs, __ T4S, b);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accepted.

tmp = *common_regs++,
n = *common_regs++;

VSeq<4> A(16);

@adinn adinn May 18, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
VSeq<4> A(16);
// vector sequences used to compute and combine partial products of
// b_i * a_j for i = {0,1,2,3} j = {3,4}
VSeq<4> A(16);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accepted.


//Load input arrays and modulus
Register a_ptr = *common_regs++, mod_ptr = *common_regs++;
__ add(a_ptr, a, 24);

@adinn adinn May 18, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
__ add(a_ptr, a, 24);
// skip 3 limbs so a_ptr addresses trailing pair {a3, a4}
__ add(a_ptr, a, 24);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accepted.

@ferakocz

Copy link
Copy Markdown
Contributor Author

/integrate

@openjdk openjdk Bot added the sponsor Pull request is ready to be sponsored label Jun 19, 2026
@openjdk

openjdk Bot commented Jun 19, 2026

Copy link
Copy Markdown

@ferakocz
Your change (at version 5b495b0) is now ready to be sponsored by a Committer.

@ferakocz

Copy link
Copy Markdown
Contributor Author

Thanks @adinn and @theRealAph! Could one of you also sponsor it?

@mrserb

mrserb commented Jun 19, 2026

Copy link
Copy Markdown
Member

Hi @ferakocz, did you have a chance to run test/micro/org/openjdk/bench/javax/crypto/full/PolynomialP256Bench.java on this patch?

.../build/patched/images/jdk/bin/java -jar .../build/patched/images/test/micro/benchmarks.jar org.openjdk.bench.javax.crypto.full.PolynomialP256Bench.benchAssign -p isMontBench=true

I got these numbers on my local laptop macOS on m4:
Patched:

PolynomialP256Bench.benchAssign true thrpt 8 10230.113 ± 146.263 ops/s

Baseline:

PolynomialP256Bench.benchAssign true thrpt 8 23548.039 ± 1596.303 ops/s

@adinn

adinn commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

@mrserb

I am also seeing a slowdown for this specific micro-benchhmark on a fedora M2 Mac:

Baseline:

PolynomialP256Bench.benchAssign true thrpt 8 14774.689 ± 1764.136 ops/s

Patched:

PolynomialP256Bench.benchAssign true thrpt 8 8171.365 ± 135.887 ops/s

The benchMultiply and benchSquare micro-benchmarks both show an improvement

Baseline:

PolynomialP256Bench.benchMultiply true thrpt 8 2624.022 ± 1.985 ops/s
PolynomialP256Bench.benchSquare true thrpt 8 2629.698 ± 3.645 ops/s

Patched:

PolynomialP256Bench.benchMultiply true thrpt 8 3200.923 ± 3.748 ops/s
PolynomialP256Bench.benchSquare true thrpt 8 3203.488 ± 3.074 ops/s

@ferakocz

I'm not sure we should automatically trust this benchmark run in isolation -- it is most important to gauge what effect the use of the multiply and assign intrinsics has when exercising the P256 API. The micro-benchmark result does suggest that the intrinsification of conditionalAssign may not always help on AArch64. However, it might still be the case that when employed in combination with the multiply intrinsic it is of benefit - possibly also depending on what hardware we are running on.

Your API/method level testing showed an improvement of 9% at the method level and 5% at the API level. Have you also run these tests on your M1 machine with the intrinsic for conditionalAssign omitted? If so what was the effect? If not then could you do so and let us know what difference it makes.

If you provide details of the tests run and how to exercise them I will happily check what the effect is on my M2 box if I disable generation of the conditionalAssign intrinsic. Perhaps @mrserb can do the same on his M4 Mac. Depending on the outcome might also want to check this on other AArch64 CPUs.

@theRealAph

Copy link
Copy Markdown
Contributor

However, it might still be the case that when employed in combination with the multiply intrinsic it is of benefit - possibly also depending on what hardware we are running on.

I agree with that, but it's surely worth a look at the generated code so see why hand-coded benchMultiply runs slower. Looking at the Java implementation, it might simply be that C2 generates near-perfect code. I just had a look, and it's not at all bad.

@theRealAph

Copy link
Copy Markdown
Contributor

hand-coded benchMultiply

Sorry, wrong method. But the point remains...

@theRealAph

Copy link
Copy Markdown
Contributor

One more thought: it might just be unrolling and inlining.

@ferakocz

Copy link
Copy Markdown
Contributor Author

One more thought: it might just be unrolling and inlining.

Apparently, it is C2 generating optimal code:
dup selector
ldr a
ldr b
bsl
str result
in the neon registers, unrolled once plus code for lengths not divisible by 4.

I haven't paid much attention to this because it is not critical for the performance of the elliptic curve computation, but it is definitely better if it is optimal.

@adinn

adinn commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Apparently, it is C2 generating optimal code.

Ok, so it seems like we don't need to generate the assign intrinsic on AArch64. @ferakocz Can you provide numbers to confirm that omitting generation of the assign intrinsic makes no difference to the crypto computation?

An interesting follow-up question is whether we need the intrinsic on x86 and if so why? It's a little more complicated since we have slightly different code depending on whether UseAVX > 2 but C2 also knows about and responds to UseAVX so one might expect it to also generate good-to-optimal code for x86.

I ran the micro benchmark on my x86 box (AMD Ryzen 9 7900 12 cores) first with the assign intrinsic generation enabled and then with it disabled and got the following results

Intrinsic enabled:

PolynomialP256Bench.benchAssign true thrpt 8 9712.115 ± 321.576 ops/s

Intrinsic disabled:

PolynomialP256Bench.benchAssign true thrpt 8 18814.815 ± 7985.590 ops/s

I'm not sure why the variance is so high in the disabled case but for this HW (where we have AVX2 and AVX512 support) it looks like a similar picture. So, . . .

@ferakocz

  1. If you can show that the assign intrinsic does not improve performance of the crytpo tests then modify this PR not to generate it and remove the generator method. That will allow us to push the multiply intrinsic.
  2. As a separate step can you or whoever implemented the x86 intrinsic check whether it gives any benefits and if not then raise a JIRA and PR either to disable generation on x86 or, if we have also dropped it for aarch64, remove it completely.

@theRealAph

Copy link
Copy Markdown
Contributor

Are both of you absolutely sure you're allowing adequate time for warmup? Sorry if this is a bit rude, but that variance looks really suspicious

@adinn

adinn commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Are both of you absolutely sure you're allowing adequate time for warmup? Sorry if this is a bit rude, but that variance looks really suspicious.

Yeah, it does look wrong . . .

Looking deeper into the test it seems that the problem here is that running the benchmark without an intrinsic is actually just inviting an apples to pears comparison. However, I did try with a longer warmup.

The benchmark test is provided in test/micro/org/openjdk/bench/javax/crypto/full/PolynomialP256Bench.java which specifies

@Warmup(iterations = 3, time = 3)

The run times for the case where there was no intrinsic were as follows

# Warmup Iteration   1: 16360.174 ops/s
# Warmup Iteration   2: 17884.640 ops/s
# Warmup Iteration   3: 15167.575 ops/s
Iteration   1: 18568.732 ops/s
Iteration   2: 18613.381 ops/s
Iteration   3: 19689.476 ops/s
Iteration   4: 25839.174 ops/s
Iteration   5: 23548.499 ops/s
Iteration   6: 14771.007 ops/s
Iteration   7: 14746.814 ops/s
Iteration   8: 14741.437 ops/s

When I ran it again with warmups set to 20 and got this

# Warmup Iteration   1: 16541.351 ops/s
# Warmup Iteration   2: 17866.159 ops/s
# Warmup Iteration   3: 15120.474 ops/s
# Warmup Iteration   4: 18522.697 ops/s
# Warmup Iteration   5: 17991.285 ops/s
# Warmup Iteration   6: 24679.450 ops/s
# Warmup Iteration   7: 17779.349 ops/s
# Warmup Iteration   8: 14321.994 ops/s
# Warmup Iteration   9: 14618.918 ops/s
# Warmup Iteration  10: 14810.129 ops/s
# Warmup Iteration  11: 14798.399 ops/s
# Warmup Iteration  12: 17931.935 ops/s
# Warmup Iteration  13: 18531.543 ops/s
# Warmup Iteration  14: 14800.131 ops/s
# Warmup Iteration  15: 15063.439 ops/s
# Warmup Iteration  16: 15064.537 ops/s
# Warmup Iteration  17: 25140.773 ops/s
# Warmup Iteration  18: 24460.440 ops/s
# Warmup Iteration  19: 21369.918 ops/s
# Warmup Iteration  20: 18584.904 ops/s
Iteration   1: 19175.972 ops/s
Iteration   2: 18969.329 ops/s
Iteration   3: 18845.385 ops/s
Iteration   4: 18518.855 ops/s
Iteration   5: 18363.983 ops/s
Iteration   6: 18085.425 ops/s
Iteration   7: 14879.353 ops/s
Iteration   8: 25664.992 ops/s

PolynomialP256Bench.benchAssign true thrpt 8 19062.912 ± 5731.302 ops/s

i.e. there is still great variability even after a lot more warmup.

The test code actually does this

    @Benchmark
    public MutableIntegerModuloP benchAssign() {
        MutableIntegerModuloP test1 = X.mutable();
        MutableIntegerModuloP test2 = one.mutable();
        for (int i = 0; i< 10000; i++) {
            test1.conditionalSet(test2, 0);
            test1.conditionalSet(test2, 1);
            test2.conditionalSet(test1, 0);
            test2.conditionalSet(test1, 1);
        }
        return test2;
    }

where IntegerPolynomial::conditionalSet is defined as

        public void conditionalSet(IntegerModuloP b, int set) {
            assert IntegerPolynomial.this == b.getField();
            Element other = (Element) b;

            conditionalAssign(set, limbs, other.limbs);
            numAdds = other.numAdds;
        }

and the intrinsic candidate IntegerPolynomial::conditionalAssign is defined as

    @ForceInline
    @IntrinsicCandidate
    protected static void conditionalAssign(int set, long[] a, long[] b) {
        int maskValue = -set;
        for (int i = 0; i < a.length; i++) {
            long dummyLimbs = maskValue & (a[i] ^ b[i]);
            a[i] = dummyLimbs ^ a[i];
        }
    }

With an intrinsic in place the loop body cannot really be optimized. The callouts to the intrinsic are opaque to the compiler so it just has to make 4 calls per iteration.

When we disable the intrinsic and compile with Java bytecode the compiler can immediately simplify inlined code for the 4 conditionalSet calls based on the set argument being 0 or 1. When set is 0 maskValue is 0 so dummyLimbs is 0 and a[i] does not need to be updated. Maybe the compiler can also work out some invariants across calls or iterations but the above is already enough to mean that the compiler will only be generating code for half the work done by the intrinsic.

That doesn't account for the high variance but it does suggest that the comparison is not valuable as it allows the compiler to win on a special case (set is known in advance).

So, @ferakocz it would still be useful to know whether disabling the intrinsic changes the results of the crypto tests you have run. That would be a good reason to consider dropping the intrinsic on aarch64 (likewise, possibly on x86). The micro-benchmark itself does not really offer any reason to do so.

@ferakocz

ferakocz commented Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

I think it is rather unfortunate that this method was added to this microbenchmark suite as its contribution to the run time of any real crypto operation is minimal, so it makes almost no difference if it runs twice as fast. However, it is important that it runs in constant time (i.e. its running time is independent of the values in its input arrays and, more importantly, whether the value of the "set" argument is 0 or 1). The java code was written in such a way, but there is no guarantee that the compiler will not change it back to using a branch instead of the xors if it can figure out that only those 2 values are possible for "set". So the intrinsic here is more for guaranteeing "set" value independent execution than for any performance gains.

@adinn

adinn commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

I think it is rather unfortunate that this method was added to this microbenchmark suite as its contribution to the run time of any real crypto operation is minimal, so it makes almost no difference if it runs twice as fast. However, it is important that it runs in constant time (i.e. its running time is independent of the values in its input arrays and, more importantly, whether the value of the "set" argument is 0 or 1). The java code was written in such a way, but there is no guarantee that the compiler will not change it back to using a branch instead of the xors if it can figure out that only those 2 values are possible for "set". So the intrinsic here is more for guaranteeing "set" value independent execution than for any performance gains.

Yes, the assign benchmark is definitely of no use for measuring performance of the intrinsic relative to the Java code given that it hard-wires set in each call. I'm not sure it is even of much interest when we do have an intrinsic for measuring a difference between cases where montBench == true and montBench == false. Whatever the input polynomial type (P256 or MontgmomeryP256) the assign is going to exercise exactly the same intrinsic code.

Your point that the intrinsic runs in constant time is the best argument for keeping it. So, I'm happy to push this as is. The same consideration would apply for x86.

@adinn

adinn commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

@theRealAph Do you agree?

@mrserb

mrserb commented Jun 23, 2026

Copy link
Copy Markdown
Member

Then maybe an existing benchmark can be update, to show some improvement or the new one added?

@adinn

adinn commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Then maybe an existing benchmark can be update, to show some improvement or the new one added?

As @ferakocz said the intrinsic is useful to ensure constant time execution in all cases. We don't actually need it to improve performance. We would only need to worry about it if it was responsible for a noticeable degradation in performance.

@ferakocz says we don't see that in the tests he has performed and I think that means we are likely to face that situation with any other kernels that rely on conditionalAssign. I'm reassured by the fact that the Java version runs roughly twice as fast as the intrinsic. That matches the opportunity the provision of bytecode offers the compiler, as noted above, to halve the number of EOR instructions. Since this would not be an option in cases where set is not hard-wired I don't think we need to care about the result of this test.

@mrserb please feel free to create a new benchmark that reliably tests the Java code vs the intrinsic for the general case (i.e. where set cannot be derived or predicted) and if there is a true degradation on either x86 or aarch64 then we can consider whether or not to keep the intrinsic.

@theRealAph

Copy link
Copy Markdown
Contributor

I take the point about constant-time implementation, but I'm still curious why an intrinsic can't match C2. Never mind, it's not important.

@adinn

adinn commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

/sponsor

@openjdk

openjdk Bot commented Jun 24, 2026

Copy link
Copy Markdown

Going to push as commit f1cd7f6.
Since your change was applied there have been 50 commits pushed to the master branch:

Your commit was automatically rebased without conflicts.

@openjdk openjdk Bot added the integrated Pull request has been integrated label Jun 24, 2026
@openjdk openjdk Bot closed this Jun 24, 2026
@openjdk openjdk Bot removed ready Pull request is ready to be integrated rfr Pull request is ready for review sponsor Pull request is ready to be sponsored labels Jun 24, 2026
@openjdk

openjdk Bot commented Jun 24, 2026

Copy link
Copy Markdown

@adinn @ferakocz Pushed as commit f1cd7f6.

💡 You may see a message that your pull request was closed with unmerged commits. This can be safely ignored.

@adinn

adinn commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

@ferakocz Thanks for your patience!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core-libs core-libs-dev@openjdk.org hotspot hotspot-dev@openjdk.org integrated Pull request has been integrated

Development

Successfully merging this pull request may close these issues.

8 participants