From c375d8a3656286617574e6f28e1f96b379c96f34 Mon Sep 17 00:00:00 2001 From: Winfried Gerlach Date: Thu, 1 Oct 2026 10:11:18 +0200 Subject: [PATCH] #2474 9x faster Pack.longToBigEndian and longToLittleEndian on JDK 23+ --- .../main/java/org/bouncycastle/util/Pack.java | 22 ++- .../bouncycastle/util/utiltest/AllTests.java | 1 + .../bouncycastle/util/utiltest/PackTest.java | 175 ++++++++++++++++++ docs/releasenotes.md | 2 + 4 files changed, 196 insertions(+), 4 deletions(-) create mode 100644 core/src/test/java/org/bouncycastle/util/utiltest/PackTest.java diff --git a/core/src/main/java/org/bouncycastle/util/Pack.java b/core/src/main/java/org/bouncycastle/util/Pack.java index 9abab94d30..28c9f02cbb 100644 --- a/core/src/main/java/org/bouncycastle/util/Pack.java +++ b/core/src/main/java/org/bouncycastle/util/Pack.java @@ -192,8 +192,15 @@ public static byte[] longToBigEndian(long n) public static void longToBigEndian(long n, byte[] bs, int off) { - intToBigEndian((int)(n >>> 32), bs, off); - intToBigEndian((int)(n & 0xffffffffL), bs, off + 4); + // #2474: on JDK 23+, this code is ~9x faster than the previous implementation (cf. JDK-8318446) + bs[off] = (byte)(n >>> 56); + bs[++off] = (byte)(n >>> 48); + bs[++off] = (byte)(n >>> 40); + bs[++off] = (byte)(n >>> 32); + bs[++off] = (byte)(n >>> 24); + bs[++off] = (byte)(n >>> 16); + bs[++off] = (byte)(n >>> 8); + bs[++off] = (byte)(n); } public static byte[] longToBigEndian(long[] ns) @@ -547,8 +554,15 @@ public static byte[] longToLittleEndian(long n) public static void longToLittleEndian(long n, byte[] bs, int off) { - intToLittleEndian((int)(n & 0xffffffffL), bs, off); - intToLittleEndian((int)(n >>> 32), bs, off + 4); + // #2474: on JDK 23+, this code is ~9x faster than the previous implementation (cf. JDK-8318446) + bs[off] = (byte)(n); + bs[++off] = (byte)(n >>> 8); + bs[++off] = (byte)(n >>> 16); + bs[++off] = (byte)(n >>> 24); + bs[++off] = (byte)(n >>> 32); + bs[++off] = (byte)(n >>> 40); + bs[++off] = (byte)(n >>> 48); + bs[++off] = (byte)(n >>> 56); } public static byte[] longToLittleEndian(long[] ns) diff --git a/core/src/test/java/org/bouncycastle/util/utiltest/AllTests.java b/core/src/test/java/org/bouncycastle/util/utiltest/AllTests.java index 170e8e6448..023c7c1031 100644 --- a/core/src/test/java/org/bouncycastle/util/utiltest/AllTests.java +++ b/core/src/test/java/org/bouncycastle/util/utiltest/AllTests.java @@ -22,6 +22,7 @@ public static Test suite() suite.addTestSuite(StringsTest.class); suite.addTestSuite(StreamsTest.class); suite.addTestSuite(AggregateRuntimeExceptionTest.class); + suite.addTestSuite(PackTest.class); return new BCTestSetup(suite); } diff --git a/core/src/test/java/org/bouncycastle/util/utiltest/PackTest.java b/core/src/test/java/org/bouncycastle/util/utiltest/PackTest.java new file mode 100644 index 0000000000..4b1cbe82ee --- /dev/null +++ b/core/src/test/java/org/bouncycastle/util/utiltest/PackTest.java @@ -0,0 +1,175 @@ +package org.bouncycastle.util.utiltest; + +import java.util.Random; + +import org.bouncycastle.util.Arrays; +import org.bouncycastle.util.Pack; + +import junit.framework.TestCase; + +public class PackTest + extends TestCase +{ + private static final byte FILL = (byte)0xA5; + + private static final long[] EXTREMES = { 0L, -1L, Long.MIN_VALUE, Long.MAX_VALUE, 0x8080808080808080L, + 0x0102030405060708L }; + + /* + * Enough calls for C2 to compile the methods under test, so that the merged stores it generates for them are + * checked as well as the interpreter. + */ + private static final int ITERATIONS = 200000; + + public void testLongToBigEndian() + { + Random random = new Random(1); + byte[] bs = new byte[24]; + for (int i = 0; i < ITERATIONS; ++i) + { + long n = nextValue(random, i); + int off = i % 9; + + Arrays.fill(bs, FILL); + Pack.longToBigEndian(n, bs, off); + checkBytes(n, true, bs, off); + if (Pack.bigEndianToLong(bs, off) != n) + { + fail("round trip of " + Long.toHexString(n)); + } + } + + assertTrue(Arrays.areEqual(new byte[]{ 1, 2, 3, 4, 5, 6, 7, 8 }, Pack.longToBigEndian(0x0102030405060708L))); + } + + public void testLongToLittleEndian() + { + Random random = new Random(2); + byte[] bs = new byte[24]; + for (int i = 0; i < ITERATIONS; ++i) + { + long n = nextValue(random, i); + int off = i % 9; + + Arrays.fill(bs, FILL); + Pack.longToLittleEndian(n, bs, off); + checkBytes(n, false, bs, off); + if (Pack.littleEndianToLong(bs, off) != n) + { + fail("round trip of " + Long.toHexString(n)); + } + } + + assertTrue(Arrays.areEqual(new byte[]{ 8, 7, 6, 5, 4, 3, 2, 1 }, Pack.longToLittleEndian(0x0102030405060708L))); + } + + public void testLongArrays() + { + Random random = new Random(3); + for (int i = 0; i < 2000; ++i) + { + long[] ns = new long[1 + random.nextInt(25)]; + for (int j = 0; j < ns.length; ++j) + { + ns[j] = nextValue(random, i + j); + } + int nsOff = random.nextInt(ns.length); + int nsLen = random.nextInt(ns.length - nsOff + 1); + int bsOff = random.nextInt(9); + int bsLen = bsOff + 8 * nsLen + random.nextInt(9); + + byte[] be = new byte[bsLen]; + Arrays.fill(be, FILL); + Pack.longToBigEndian(ns, nsOff, nsLen, be, bsOff); + byte[] le = new byte[bsLen]; + Arrays.fill(le, FILL); + Pack.longToLittleEndian(ns, nsOff, nsLen, le, bsOff); + for (int j = 0; j < nsLen; ++j) + { + assertEquals(ns[nsOff + j], Pack.bigEndianToLong(be, bsOff + 8 * j)); + assertEquals(ns[nsOff + j], Pack.littleEndianToLong(le, bsOff + 8 * j)); + } + checkUntouched(be, 0, bsOff); + checkUntouched(be, bsOff + 8 * nsLen, bsLen); + checkUntouched(le, 0, bsOff); + checkUntouched(le, bsOff + 8 * nsLen, bsLen); + + long[] part = Arrays.copyOfRange(ns, nsOff, nsOff + nsLen); + assertTrue(Arrays.areEqual(Arrays.copyOfRange(be, bsOff, bsOff + 8 * nsLen), Pack.longToBigEndian(part))); + assertTrue(Arrays.areEqual(Arrays.copyOfRange(le, bsOff, bsOff + 8 * nsLen), Pack.longToLittleEndian(part))); + } + } + + public void testOutOfBounds() + { + for (int len = 0; len < 8; ++len) + { + try + { + Pack.longToBigEndian(-1L, new byte[len], 0); + fail("no exception for " + len + " bytes"); + } + catch (ArrayIndexOutOfBoundsException e) + { + // expected + } + try + { + Pack.longToLittleEndian(-1L, new byte[len], 0); + fail("no exception for " + len + " bytes"); + } + catch (ArrayIndexOutOfBoundsException e) + { + // expected + } + } + } + + /** + * Check the 8 bytes at off against the definition of the encoding, one byte at a time, and that the bytes around + * them are untouched. + */ + private static void checkBytes(long n, boolean bigEndian, byte[] bs, int off) + { + for (int i = 0; i < 8; ++i) + { + int shift = bigEndian ? 56 - 8 * i : 8 * i; + if (bs[off + i] != (byte)(n >>> shift)) + { + fail("byte " + i + " of " + Long.toHexString(n) + (bigEndian ? " big" : " little") + " endian"); + } + } + checkUntouched(bs, 0, off); + checkUntouched(bs, off + 8, bs.length); + } + + private static void checkUntouched(byte[] bs, int from, int to) + { + for (int i = from; i < to; ++i) + { + if (bs[i] != FILL) + { + fail("byte " + i + " overwritten"); + } + } + } + + /** + * Random values, values with a single byte set or clear at each position in turn, and extremes. + */ + private static long nextValue(Random random, int i) + { + long oneByte = (long)(1 + random.nextInt(255)) << (8 * ((i >> 2) & 7)); + switch (i & 3) + { + case 0: + return random.nextLong(); + case 1: + return oneByte; + case 2: + return ~oneByte; + default: + return EXTREMES[(i >> 2) % EXTREMES.length]; + } + } +} diff --git a/docs/releasenotes.md b/docs/releasenotes.md index 3983bd59b6..5a8e98dd7b 100644 --- a/docs/releasenotes.md +++ b/docs/releasenotes.md @@ -81,6 +81,8 @@ Date: 2026, TBD - 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. +- 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%. + ### 2.1.4 Additional Notes - 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//**/*.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.