Skip to content

Commit c375d8a

Browse files
committed
#2474 9x faster Pack.longToBigEndian and longToLittleEndian on JDK 23+
1 parent 6d7d611 commit c375d8a

4 files changed

Lines changed: 196 additions & 4 deletions

File tree

‎core/src/main/java/org/bouncycastle/util/Pack.java‎

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -192,8 +192,15 @@ public static byte[] longToBigEndian(long n)
192192

193193
public static void longToBigEndian(long n, byte[] bs, int off)
194194
{
195-
intToBigEndian((int)(n >>> 32), bs, off);
196-
intToBigEndian((int)(n & 0xffffffffL), bs, off + 4);
195+
// #2474: on JDK 23+, this code is ~9x faster than the previous implementation (cf. JDK-8318446)
196+
bs[off] = (byte)(n >>> 56);
197+
bs[++off] = (byte)(n >>> 48);
198+
bs[++off] = (byte)(n >>> 40);
199+
bs[++off] = (byte)(n >>> 32);
200+
bs[++off] = (byte)(n >>> 24);
201+
bs[++off] = (byte)(n >>> 16);
202+
bs[++off] = (byte)(n >>> 8);
203+
bs[++off] = (byte)(n);
197204
}
198205

199206
public static byte[] longToBigEndian(long[] ns)
@@ -547,8 +554,15 @@ public static byte[] longToLittleEndian(long n)
547554

548555
public static void longToLittleEndian(long n, byte[] bs, int off)
549556
{
550-
intToLittleEndian((int)(n & 0xffffffffL), bs, off);
551-
intToLittleEndian((int)(n >>> 32), bs, off + 4);
557+
// #2474: on JDK 23+, this code is ~9x faster than the previous implementation (cf. JDK-8318446)
558+
bs[off] = (byte)(n);
559+
bs[++off] = (byte)(n >>> 8);
560+
bs[++off] = (byte)(n >>> 16);
561+
bs[++off] = (byte)(n >>> 24);
562+
bs[++off] = (byte)(n >>> 32);
563+
bs[++off] = (byte)(n >>> 40);
564+
bs[++off] = (byte)(n >>> 48);
565+
bs[++off] = (byte)(n >>> 56);
552566
}
553567

554568
public static byte[] longToLittleEndian(long[] ns)

‎core/src/test/java/org/bouncycastle/util/utiltest/AllTests.java‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public static Test suite()
2222
suite.addTestSuite(StringsTest.class);
2323
suite.addTestSuite(StreamsTest.class);
2424
suite.addTestSuite(AggregateRuntimeExceptionTest.class);
25+
suite.addTestSuite(PackTest.class);
2526
return new BCTestSetup(suite);
2627
}
2728

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
package org.bouncycastle.util.utiltest;
2+
3+
import java.util.Random;
4+
5+
import org.bouncycastle.util.Arrays;
6+
import org.bouncycastle.util.Pack;
7+
8+
import junit.framework.TestCase;
9+
10+
public class PackTest
11+
extends TestCase
12+
{
13+
private static final byte FILL = (byte)0xA5;
14+
15+
private static final long[] EXTREMES = { 0L, -1L, Long.MIN_VALUE, Long.MAX_VALUE, 0x8080808080808080L,
16+
0x0102030405060708L };
17+
18+
/*
19+
* Enough calls for C2 to compile the methods under test, so that the merged stores it generates for them are
20+
* checked as well as the interpreter.
21+
*/
22+
private static final int ITERATIONS = 200000;
23+
24+
public void testLongToBigEndian()
25+
{
26+
Random random = new Random(1);
27+
byte[] bs = new byte[24];
28+
for (int i = 0; i < ITERATIONS; ++i)
29+
{
30+
long n = nextValue(random, i);
31+
int off = i % 9;
32+
33+
Arrays.fill(bs, FILL);
34+
Pack.longToBigEndian(n, bs, off);
35+
checkBytes(n, true, bs, off);
36+
if (Pack.bigEndianToLong(bs, off) != n)
37+
{
38+
fail("round trip of " + Long.toHexString(n));
39+
}
40+
}
41+
42+
assertTrue(Arrays.areEqual(new byte[]{ 1, 2, 3, 4, 5, 6, 7, 8 }, Pack.longToBigEndian(0x0102030405060708L)));
43+
}
44+
45+
public void testLongToLittleEndian()
46+
{
47+
Random random = new Random(2);
48+
byte[] bs = new byte[24];
49+
for (int i = 0; i < ITERATIONS; ++i)
50+
{
51+
long n = nextValue(random, i);
52+
int off = i % 9;
53+
54+
Arrays.fill(bs, FILL);
55+
Pack.longToLittleEndian(n, bs, off);
56+
checkBytes(n, false, bs, off);
57+
if (Pack.littleEndianToLong(bs, off) != n)
58+
{
59+
fail("round trip of " + Long.toHexString(n));
60+
}
61+
}
62+
63+
assertTrue(Arrays.areEqual(new byte[]{ 8, 7, 6, 5, 4, 3, 2, 1 }, Pack.longToLittleEndian(0x0102030405060708L)));
64+
}
65+
66+
public void testLongArrays()
67+
{
68+
Random random = new Random(3);
69+
for (int i = 0; i < 2000; ++i)
70+
{
71+
long[] ns = new long[1 + random.nextInt(25)];
72+
for (int j = 0; j < ns.length; ++j)
73+
{
74+
ns[j] = nextValue(random, i + j);
75+
}
76+
int nsOff = random.nextInt(ns.length);
77+
int nsLen = random.nextInt(ns.length - nsOff + 1);
78+
int bsOff = random.nextInt(9);
79+
int bsLen = bsOff + 8 * nsLen + random.nextInt(9);
80+
81+
byte[] be = new byte[bsLen];
82+
Arrays.fill(be, FILL);
83+
Pack.longToBigEndian(ns, nsOff, nsLen, be, bsOff);
84+
byte[] le = new byte[bsLen];
85+
Arrays.fill(le, FILL);
86+
Pack.longToLittleEndian(ns, nsOff, nsLen, le, bsOff);
87+
for (int j = 0; j < nsLen; ++j)
88+
{
89+
assertEquals(ns[nsOff + j], Pack.bigEndianToLong(be, bsOff + 8 * j));
90+
assertEquals(ns[nsOff + j], Pack.littleEndianToLong(le, bsOff + 8 * j));
91+
}
92+
checkUntouched(be, 0, bsOff);
93+
checkUntouched(be, bsOff + 8 * nsLen, bsLen);
94+
checkUntouched(le, 0, bsOff);
95+
checkUntouched(le, bsOff + 8 * nsLen, bsLen);
96+
97+
long[] part = Arrays.copyOfRange(ns, nsOff, nsOff + nsLen);
98+
assertTrue(Arrays.areEqual(Arrays.copyOfRange(be, bsOff, bsOff + 8 * nsLen), Pack.longToBigEndian(part)));
99+
assertTrue(Arrays.areEqual(Arrays.copyOfRange(le, bsOff, bsOff + 8 * nsLen), Pack.longToLittleEndian(part)));
100+
}
101+
}
102+
103+
public void testOutOfBounds()
104+
{
105+
for (int len = 0; len < 8; ++len)
106+
{
107+
try
108+
{
109+
Pack.longToBigEndian(-1L, new byte[len], 0);
110+
fail("no exception for " + len + " bytes");
111+
}
112+
catch (ArrayIndexOutOfBoundsException e)
113+
{
114+
// expected
115+
}
116+
try
117+
{
118+
Pack.longToLittleEndian(-1L, new byte[len], 0);
119+
fail("no exception for " + len + " bytes");
120+
}
121+
catch (ArrayIndexOutOfBoundsException e)
122+
{
123+
// expected
124+
}
125+
}
126+
}
127+
128+
/**
129+
* Check the 8 bytes at off against the definition of the encoding, one byte at a time, and that the bytes around
130+
* them are untouched.
131+
*/
132+
private static void checkBytes(long n, boolean bigEndian, byte[] bs, int off)
133+
{
134+
for (int i = 0; i < 8; ++i)
135+
{
136+
int shift = bigEndian ? 56 - 8 * i : 8 * i;
137+
if (bs[off + i] != (byte)(n >>> shift))
138+
{
139+
fail("byte " + i + " of " + Long.toHexString(n) + (bigEndian ? " big" : " little") + " endian");
140+
}
141+
}
142+
checkUntouched(bs, 0, off);
143+
checkUntouched(bs, off + 8, bs.length);
144+
}
145+
146+
private static void checkUntouched(byte[] bs, int from, int to)
147+
{
148+
for (int i = from; i < to; ++i)
149+
{
150+
if (bs[i] != FILL)
151+
{
152+
fail("byte " + i + " overwritten");
153+
}
154+
}
155+
}
156+
157+
/**
158+
* Random values, values with a single byte set or clear at each position in turn, and extremes.
159+
*/
160+
private static long nextValue(Random random, int i)
161+
{
162+
long oneByte = (long)(1 + random.nextInt(255)) << (8 * ((i >> 2) & 7));
163+
switch (i & 3)
164+
{
165+
case 0:
166+
return random.nextLong();
167+
case 1:
168+
return oneByte;
169+
case 2:
170+
return ~oneByte;
171+
default:
172+
return EXTREMES[(i >> 2) % EXTREMES.length];
173+
}
174+
}
175+
}

‎docs/releasenotes.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ Date: 2026, TBD
8181

8282
- The BCJSSE provider adds an org.bouncycastle.jsse.BCSSLContext interface exposing extended functionality of its SSLContext, obtained with org.bouncycastle.jsse.util.ContextUtil.getBCSSLContext() by way of the new BCSSLSessionContext interface the context's session contexts implement. Its getDefaultParameters(boolean) and getSupportedParameters(boolean) return the context's default and supported parameters as a BCSSLParameters for either client or server mode, including the BC-specific properties, where SSLContext.getDefaultSSLParameters() and getSupportedSSLParameters() report client mode only and cannot carry those properties. A BCSSLContext describes the initialization of the SSLContext it was obtained from, and is not updated if the SSLContext is re-initialized.
8383

84+
- org.bouncycastle.util.Pack.longToBigEndian() and longToLittleEndian() rewritten so it can be optimized by JDK 23+'s JIT. JDK 25 measurements show packing longs into bytes is ~9x as fast, which makes squeezing SHAKE output about 8% faster per block, a 64-byte SHA-512 or BLAKE2b digest 6-7% faster and ML-DSA key generation and verification 3-5% faster; JDK 17 and 21 performance decreases slighty, within 3%.
85+
8486
### 2.1.4 Additional Notes
8587

8688
- The sources and javadoc jars of the Ant-built distributions (jdk14, jdk15to18 and jdk13) no longer carry test material. Each module's javadoc target copies the package documentation it needs - org/bouncycastle/<area>/**/*.html - back into the module source directory that has already been compiled from, and zip-src zips that directory afterwards, so every test package's package.html arrived in the sources jar by that route; javadoc-util additionally copied org/bouncycastle/asn1/isismtt/**/*.java, which put test classes into the bcutil javadoc as generated pages, and javadoc-pg deliberately copied the gpg and bcpg test sources in order to document them. Separately the source copies excluded test material only one directory deep and only for *.java, because Ant reads ** as an any-depth wildcard just where it is a whole path segment, so anything nested further or with another extension - the PEM certificate fixtures under org/bouncycastle/est/test/san corrected in 1.86, and an ICAO master list under org/bouncycastle/asn1/icao/test - went through. The source and javadoc copies of every module now exclude test directories at any depth, and javadoc-pg no longer documents the test packages. org.bouncycastle.util.test is unaffected and still ships in the bcprov binary, sources and javadoc jars, as it does from the Gradle build: it is the SimpleTest framework the light-weight API's own test classes are written against, not test material of the distribution. No binary changes - the classes and resources of every Ant-built jar are identical to those of the 1.86 release - and the Gradle-built jdk18on artifacts never carried any of this.

0 commit comments

Comments
 (0)