RFR: 8294248: Use less limbs for P256 in EC implementation
Daniel Jeliński
djelinski at openjdk.org
Sat Sep 24 09:38:23 UTC 2022
On Sat, 24 Sep 2022 08:17:56 GMT, Xue-Lei Andrew Fan <xuelei at openjdk.org> wrote:
>> Hi,
>>
>> Please review this performance improvement for Secp256R1 implementation in OpenJDK. With this update, there is an about 20% performance improvement for Secp256R1 key generation and signature.
>>
>> Basically, 256 bits EC curves could use 9 integer limbs for the computation. The current implementation use 10 limbs instead. By reducing the number of limbs, the implementation could benefit from less integer computation (add/sub/multiply/square/inverse/mod/pow, etc), and thus improve the performance.
>>
>> Here are the benchmark numbers without the patch:
>>
>> Benchmark (messageLength) Mode Cnt Score Error Units
>> Signatures.sign 64 thrpt 15 1.414 ± 0.022 ops/ms
>> Signatures.sign 512 thrpt 15 1.418 ± 0.004 ops/ms
>> Signatures.sign 2048 thrpt 15 1.419 ± 0.005 ops/ms
>> Signatures.sign 16384 thrpt 15 1.395 ± 0.003 ops/ms
>>
>> KeyGenerators.keyPairGen thrpt 15 1.475 ± 0.043 ops/ms
>>
>>
>> And here are the numbers with the patch applied:
>>
>> Benchmark (messageLength) Mode Cnt Score Error Units
>> ECSignature.sign 64 thrpt 15 1.719 ± 0.010 ops/ms
>> ECSignature.sign 512 thrpt 15 1.704 ± 0.012 ops/ms
>> ECSignature.sign 2048 thrpt 15 1.699 ± 0.018 ops/ms
>> ECSignature.sign 16384 thrpt 15 1.681 ± 0.006 ops/ms
>>
>> KeyGenerators.keyPairGen thrpt 15 1.881 ± 0.008 ops/ms
>>
>>
>> Thanks,
>> Xuelei
>
> I check with the real bytes for the following computation. For Secp256R1, the p is defined.
>
> p = FFFFFFFF 00000001 00000000 00000000 00000000 FFFFFFFF FFFFFFFF FFFFFFFF
> = 2^224(2^32 −1) + 2^192 + 2^96 − 1
> = 2^256 - 2^224 + 2^192 + 2^96 − 1
>
>
> Let's use a full 256 bits integer for following computation, which is bigger than p:
>
>
> FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF
>
>> starting with 29 bit limbs
>
> With 29 bit limbs, it could be expressed in 9 29-bit words in little-endian order, as the following:
>
>
> a[] = 1FFFFFFF 1FFFFFFF 1FFFFFFF 1FFFFFFF 1FFFFFFF 1FFFFFFF 1FFFFFFF 1FFFFFFF 00FFFFFF
>
>
> From a[0] to a[7], 29 bits for each limbs, and a[8] is 24 bits. 29 * 8 + 24 = 256
>
>> after 2 additions (maxAdds) we have up to 31 bits
>
> after 2 additions, the 9-29 bits words are:
>
> a[] = 7FFFFFFF 7FFFFFFF 7FFFFFFF 7FFFFFFF 7FFFFFFF 7FFFFFFF 7FFFFFFF 7FFFFFFF 03FFFFFF
>
> From a[0] to a[7], 31 bits for each limbs, and a[8] is 26 bits. This is the biggest number we could get from the finite filed computation.
>
>> then we multiply limbs (62 bits) and add up to numLimbs (9) results together = 65 bits, which overflows long
>> and we didn't start reducing yet.
>
> Let's multiply two limbs:
>
> a[] = 7FFFFFFF 7FFFFFFF 7FFFFFFF 7FFFFFFF 7FFFFFFF 7FFFFFFF 7FFFFFFF 7FFFFFFF 03FFFFFF
> b[] = 7FFFFFFF 7FFFFFFF 7FFFFFFF 7FFFFFFF 7FFFFFFF 7FFFFFFF 7FFFFFFF 7FFFFFFF 03FFFFFF
>
>
> The overflow we cared about is the following computation, the longest additions.
>
> long c8 = (a[0] * b[8]) + (a[1] * b[7]) + (a[2] * b[6]) + (a[3] * b[5]) + (a[4] * b[4]) + (a[5] * b[3]) + (a[6] * b[2]) + (a[7] * b[1]) + (a[8] * b[0]);
>
> Now we have a[0]-a[7] are 31-bit words, and b[0]-b[7] are 31-bit words. a[8] and b[8] is 26-bit words.
>
> Then, we know that (a[0] * b[8]) and (a[8] * b[0]) are multiplying between 31-bits words and 26-bits words. So we have the equivalent computation for the a and b values above:
>
> a[1]=a[2]=a[3]=a[4]=a[5]=a[6]=a[7]=0x7FFFFFFF;
> b[1]=b[2]=b[3]=b[4]=b[5]=b[6]=b[7]=0x7FFFFFFF;
> long i1x7 = (a[1] * b[7]) + (a[2] * b[6]) + (a[3] * b[5]) + (a[4] * b[4]) + (a[5] * b[3]) + (a[6] * b[2]) + (a[7] * b[1])
> = 0xBFFFFFF900000007L
> long i0x8 = (a[0] * b[8]) + (a[8] * b[0]);
> = 0x03FFFFFEF8000002L
>
> long c8 = i1x7 + i0x8
> = 0xC3FFFFF7F8000009L
>
>
> c8 fills the 64 bits fully, but it does not overflow.
>
> @djelinski does it sound right to you?
>
> Although there is not overflow if I get the above computation right, it is still not a straightforward thing we can know without detailed computation. Instead, this kind of computation and checking could be performed in FieldGen code automatically, or just reduce limbs operations result (up to no more than twice the modulus) and remove the limit of maxAdd.
>
> This RFE, JDK-8294248, is one of a few performance improvement of the EC implementation in JDK ([JDK-8294188](https://bugs.openjdk.org/browse/JDK-8294188)). I would like to address the checking or/and reducing improvement in a separately RFE.
@XueleiFan as far as I can tell, we cannot assume that the starting number will be 256 bit; the `setReduced` operation performs a limited reduce that only ensures that limbs fit in bitsPerLimb; the carry from the last limb is only performed in `finalCarryReduceLast`.
If we start with 29 ones in every limb, the computation will overflow.
> Instead, this kind of computation and checking could be performed in FieldGen code automatically, or just reduce limbs operations result (up to no more than twice the modulus) and remove the limit of maxAdd.
Both options are worth exploring.
Thanks for working on this!
-------------
PR: https://git.openjdk.org/jdk/pull/10398
More information about the security-dev
mailing list