This is an automated email from the ASF dual-hosted git repository.
aherbert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-rng.git
The following commit(s) were added to refs/heads/master by this push:
new d73cbc42 RNG-191: Dynamically call Math multiply high methods
d73cbc42 is described below
commit d73cbc42d380ab63fdeadb83eebf6105ea573373
Author: Alex Herbert <[email protected]>
AuthorDate: Tue Mar 3 14:35:11 2026 +0000
RNG-191: Dynamically call Math multiply high methods
Adds unsignedMultiplyHigh methods potentially using native 128-bit
multiplication to the Philox4x64 generator.
---
commons-rng-core/pom.xml | 6 +
.../commons/rng/core/source64/Philox4x64.java | 4 +-
.../commons/rng/core/source64/PhiloxSupport.java | 180 +++++++++++++++++++++
.../commons/rng/core/source64/LXMSupportTest.java | 2 +-
.../rng/core/source64/PhiloxSupportTest.java | 79 +++++++++
src/conf/checkstyle/checkstyle-suppressions.xml | 2 +
src/conf/pmd/pmd-ruleset.xml | 10 +-
7 files changed, 279 insertions(+), 4 deletions(-)
diff --git a/commons-rng-core/pom.xml b/commons-rng-core/pom.xml
index f0de437e..c6fc94fc 100644
--- a/commons-rng-core/pom.xml
+++ b/commons-rng-core/pom.xml
@@ -55,6 +55,12 @@
<!-- Change from commons-parent of 1.0 as some illegal state cases cannot
be reached -->
<commons.jacoco.instructionRatio>0.99</commons.jacoco.instructionRatio>
<commons.jacoco.lineRatio>0.99</commons.jacoco.lineRatio>
+ <!-- Change from commons-parent of 1.0 as method handles to JDK Math
cannot always be executed -->
+ <commons.jacoco.methodRatio>0.99</commons.jacoco.methodRatio>
+ <commons.jacoco.complexityRatio>0.99</commons.jacoco.complexityRatio>
+
+ <!-- Disable to allow use of MethodHandle to higher JDK version methods.
-->
+ <animal.sniffer.skip>true</animal.sniffer.skip>
</properties>
<dependencies>
diff --git
a/commons-rng-core/src/main/java/org/apache/commons/rng/core/source64/Philox4x64.java
b/commons-rng-core/src/main/java/org/apache/commons/rng/core/source64/Philox4x64.java
index 813ef02c..8e92925d 100644
---
a/commons-rng-core/src/main/java/org/apache/commons/rng/core/source64/Philox4x64.java
+++
b/commons-rng-core/src/main/java/org/apache/commons/rng/core/source64/Philox4x64.java
@@ -239,9 +239,9 @@ public final class Philox4x64 extends LongProvider
implements LongJumpableUnifor
*/
private static void singleRound(long[] counter, long key0, long key1) {
final long lo0 = PHILOX_M0 * counter[0];
- final long hi0 = LXMSupport.unsignedMultiplyHigh(PHILOX_M0,
counter[0]);
+ final long hi0 = PhiloxSupport.unsignedMultiplyHigh(PHILOX_M0,
counter[0]);
final long lo1 = PHILOX_M1 * counter[2];
- final long hi1 = LXMSupport.unsignedMultiplyHigh(PHILOX_M1,
counter[2]);
+ final long hi1 = PhiloxSupport.unsignedMultiplyHigh(PHILOX_M1,
counter[2]);
counter[0] = hi1 ^ counter[1] ^ key0;
counter[1] = lo1;
diff --git
a/commons-rng-core/src/main/java/org/apache/commons/rng/core/source64/PhiloxSupport.java
b/commons-rng-core/src/main/java/org/apache/commons/rng/core/source64/PhiloxSupport.java
new file mode 100644
index 00000000..35a2a7bb
--- /dev/null
+++
b/commons-rng-core/src/main/java/org/apache/commons/rng/core/source64/PhiloxSupport.java
@@ -0,0 +1,180 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.rng.core.source64;
+
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+import java.util.function.LongBinaryOperator;
+import java.util.stream.Stream;
+
+/**
+ * Utility support for the Philox family of generators.
+ *
+ * <p>Contains methods that use the {@code java.lang.invoke} package to call
+ * {@code java.lang.Math} functions for computing the high part of the 128-bit
+ * result of a multiply of two 64-bit longs. These methods may be supported
+ * by intrinsic calls to native operations if supported on the platform for
+ * a significant performance gain.
+ *
+ * <p>Note
+ *
+ * <p>This class is used specifically in the {@link Philox4x64} generator which
+ * has a state update cycle which is performance dependent on the multiply
+ * of two unsigned long values. Other classes which use unsigned multiply
+ * and are not performance dependent on the method do not use this
implementation
+ * (for example the LXM family of generators). This allows the multiply method
+ * to be adapted to the usage of {@link Philox4x64} which always has the first
+ * argument as a negative constant.
+ *
+ * @since 1.7
+ */
+final class PhiloxSupport {
+ /**
+ * Method to compute unsigned multiply high. Uses:
+ * <ul>
+ * <li>{@code java.lang.Math.unsignedMultiplyHigh} if Java 18
+ * <li>{@code java.lang.Math.multiplyHigh} if Java 9
+ * <li>otherwise a default implementation.
+ * </ul>
+ */
+ private static final LongBinaryOperator UNSIGNED_MULTIPLY_HIGH;
+
+ static {
+ // Note:
+ // This uses the public lookup mechanism for static methods to find
methods
+ // added to java.lang.Math since java 8 to make them available in java
8.
+ // For simplicity the lookup is always attempted rather than checking
the
+ // the java version from System.getProperty("java.version").
+ final LongBinaryOperator op1 = getMathUnsignedMultiplyHigh();
+ final LongBinaryOperator op2 = getMathMultiplyHigh();
+ UNSIGNED_MULTIPLY_HIGH = Stream.of(op1, op2)
+ .filter(PhiloxSupport::testUnsignedMultiplyHigh)
+ .findFirst()
+ .orElse(LXMSupport::unsignedMultiplyHigh);
+ }
+
+ /** No instances. */
+ private PhiloxSupport() {}
+
+ /**
+ * Gets a method to compute the high 64-bits of an unsigned 64-bit
multiplication
+ * using the Math unsignedMultiplyHigh method from JDK 18.
+ *
+ * @return the method, or null
+ */
+ private static LongBinaryOperator getMathUnsignedMultiplyHigh() {
+ try {
+ // JDK 18 method
+ final MethodHandle mh = getMathMethod("unsignedMultiplyHigh");
+ return (a, b) -> {
+ try {
+ return (long) mh.invokeExact(a, b);
+ } catch (Throwable ignored) {
+ throw new IllegalStateException("Cannot invoke
Math.unsignedMultiplyHigh");
+ }
+ };
+ } catch (NoSuchMethodException | IllegalAccessException ignored) {
+ return null;
+ }
+ }
+
+ /**
+ * Gets a method to compute the high 64-bits of an unsigned 64-bit
multiplication
+ * using the Math multiplyHigh method from JDK 9.
+ *
+ * @return the method, or null
+ */
+ private static LongBinaryOperator getMathMultiplyHigh() {
+ try {
+ // JDK 9 method
+ final MethodHandle mh = getMathMethod("multiplyHigh");
+ return (a, b) -> {
+ try {
+ // Correct signed result to unsigned.
+ // Assume a is negative, but use sign bit to check b is
negative.
+ return (long) mh.invokeExact(a, b) + b + ((b >> 63) & a);
+ } catch (Throwable ignored) {
+ throw new IllegalStateException("Cannot invoke
Math.multiplyHigh");
+ }
+ };
+ } catch (NoSuchMethodException | IllegalAccessException ignored) {
+ return null;
+ }
+ }
+
+ /**
+ * Gets the named method from the {@link Math} class.
+ *
+ * <p>The look-up assumes the named method accepts two long arguments and
returns
+ * a long.
+ *
+ * @param methodName Method name.
+ * @return the method
+ * @throws NoSuchMethodException if the method does not exist
+ * @throws IllegalAccessException if the method cannot be accessed
+ */
+ static MethodHandle getMathMethod(String methodName) throws
NoSuchMethodException, IllegalAccessException {
+ return MethodHandles.publicLookup()
+ .findStatic(Math.class,
+ methodName,
+ MethodType.methodType(long.class, long.class,
long.class));
+ }
+
+ /**
+ * Test the implementation of unsigned multiply high.
+ * It is assumed the invocation of the method may raise an {@link
IllegalStateException}
+ * if it cannot be invoked.
+ *
+ * @param op Method implementation.
+ * @return True if the method can be called to generate the expected result
+ */
+ static boolean testUnsignedMultiplyHigh(LongBinaryOperator op) {
+ try {
+ // Test with a signed input to the multiplication.
+ // The result is: (1L << 63) * 2 == 1LL << 64
+ return op != null && op.applyAsLong(Long.MIN_VALUE, 2L) == 1;
+ } catch (IllegalStateException ignored) {
+ return false;
+ }
+ }
+
+ /**
+ * Multiply the two values as if unsigned 64-bit longs to produce the high
64-bits
+ * of the 128-bit unsigned result. The first argument is assumed to be
negative.
+ *
+ * <p>This method uses a {@link MethodHandle} to call Java functions added
since
+ * Java 8 to the {@link Math} class:
+ * <ul>
+ * <li>{@code java.lang.Math.unsignedMultiplyHigh} if Java 18
+ * <li>{@code java.lang.Math.multiplyHigh} if Java 9
+ * <li>otherwise a default implementation.
+ * </ul>
+ *
+ * <p><strong>Warning</strong>
+ *
+ * <p>For performance reasons this method assumes the first argument is
negative.
+ * This allows some operations to be dropped if running on Java 9 to 17.
+ *
+ * @param value1 the first value (must be negative)
+ * @param value2 the second value
+ * @return the high 64-bits of the 128-bit result
+ */
+ static long unsignedMultiplyHigh(long value1, long value2) {
+ return UNSIGNED_MULTIPLY_HIGH.applyAsLong(value1, value2);
+ }
+}
diff --git
a/commons-rng-core/src/test/java/org/apache/commons/rng/core/source64/LXMSupportTest.java
b/commons-rng-core/src/test/java/org/apache/commons/rng/core/source64/LXMSupportTest.java
index 8a8138cf..6da9258c 100644
---
a/commons-rng-core/src/test/java/org/apache/commons/rng/core/source64/LXMSupportTest.java
+++
b/commons-rng-core/src/test/java/org/apache/commons/rng/core/source64/LXMSupportTest.java
@@ -108,7 +108,7 @@ class LXMSupportTest {
}
}
- private static void assertMultiplyHigh(long v1, long v2, long hi) {
+ static void assertMultiplyHigh(long v1, long v2, long hi) {
final BigInteger bi1 = toUnsignedBigInteger(v1);
final BigInteger bi2 = toUnsignedBigInteger(v2);
final BigInteger expected = bi1.multiply(bi2);
diff --git
a/commons-rng-core/src/test/java/org/apache/commons/rng/core/source64/PhiloxSupportTest.java
b/commons-rng-core/src/test/java/org/apache/commons/rng/core/source64/PhiloxSupportTest.java
new file mode 100644
index 00000000..cbccb85a
--- /dev/null
+++
b/commons-rng-core/src/test/java/org/apache/commons/rng/core/source64/PhiloxSupportTest.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.rng.core.source64;
+
+import java.util.SplittableRandom;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for {@link PhiloxSupport}.
+ */
+class PhiloxSupportTest {
+ @Test
+ void testGetMathMethod() throws NoSuchMethodException,
IllegalAccessException {
+ // Java 8 method: Math.addExact(long, long)
+ Assertions.assertNotNull(PhiloxSupport.getMathMethod("addExact"));
+ Assertions.assertThrows(NoSuchMethodException.class, () ->
PhiloxSupport.getMathMethod("foo"));
+ }
+
+ @Test
+ void testTestUnsignedMultiplyHigh() {
+
Assertions.assertTrue(PhiloxSupport.testUnsignedMultiplyHigh(LXMSupport::unsignedMultiplyHigh));
+ // Test all code paths
+ Assertions.assertFalse(PhiloxSupport.testUnsignedMultiplyHigh(null),
"Null operator");
+ Assertions.assertFalse(PhiloxSupport.testUnsignedMultiplyHigh((a, b)
-> 0), "Invalid multiply operator");
+ Assertions.assertFalse(PhiloxSupport.testUnsignedMultiplyHigh((a, b)
-> {
+ throw new IllegalStateException();
+ }), "Illegal call to operator");
+ }
+
+ @Test
+ void testUnsignedMultiplyHighEdgeCases() {
+ final long[] values = {
+ -1, 0, 1, Long.MAX_VALUE, Long.MIN_VALUE,
+ 0xffL, 0xff00L, 0xff0000L, 0xff000000L,
+ 0xff00000000L, 0xff0000000000L, 0xff000000000000L,
0xff000000000000L,
+ 0xffffL, 0xffff0000L, 0xffff00000000L, 0xffff000000000000L,
+ 0xffffffffL, 0xffffffff00000000L,
+ // Philox 4x64 multiplication constants
+ 0xD2E7470EE14C6C93L, 0xCA5A826395121157L,
+ };
+
+ for (final long v1 : values) {
+ // Must be odd
+ if (v1 >= 0) {
+ continue;
+ }
+ for (final long v2 : values) {
+ LXMSupportTest.assertMultiplyHigh(v1, v2,
PhiloxSupport.unsignedMultiplyHigh(v1, v2));
+ }
+ }
+ }
+
+ @Test
+ void testUnsignedMultiplyHigh() {
+ final long[] values = new SplittableRandom().longs(100).toArray();
+ for (long v1 : values) {
+ // Must be odd
+ v1 |= Long.MIN_VALUE;
+ for (final long v2 : values) {
+ LXMSupportTest.assertMultiplyHigh(v1, v2,
PhiloxSupport.unsignedMultiplyHigh(v1, v2));
+ }
+ }
+ }
+}
diff --git a/src/conf/checkstyle/checkstyle-suppressions.xml
b/src/conf/checkstyle/checkstyle-suppressions.xml
index f9c106c7..fe523ec8 100644
--- a/src/conf/checkstyle/checkstyle-suppressions.xml
+++ b/src/conf/checkstyle/checkstyle-suppressions.xml
@@ -26,6 +26,8 @@
<suppress checks="UnnecessaryParentheses"
files=".*stress[/\\]StressTestCommand\.java$" lines="696" />
<!-- Special to allow withUniformRandomProvider to act as a constructor. -->
<suppress checks="HiddenField" files=".*Sampler\.java$" message="'rng' hides
a field." />
+ <!-- Invocation of MethodHandle raises Throwable. -->
+ <suppress checks="IllegalCatch" files="source64[\\/]PhiloxSupport\.java$"/>
<!-- Methods have the names from the Spliterator interface that is
implemented by child classes.
Classes are package-private and should not require documentation. -->
<suppress checks="MissingJavadocMethod"
files="[\\/]UniformRandomProviderSupport\.java$" lines="479-484"/>
diff --git a/src/conf/pmd/pmd-ruleset.xml b/src/conf/pmd/pmd-ruleset.xml
index 94d113a7..a716e49c 100644
--- a/src/conf/pmd/pmd-ruleset.xml
+++ b/src/conf/pmd/pmd-ruleset.xml
@@ -120,7 +120,8 @@
or @SimpleName='Coordinates' or @SimpleName='Hex' or
@SimpleName='SpecialMath'
or @SimpleName='Conversions' or @SimpleName='MixFunctions' or
@SimpleName='LXMSupport'
or @SimpleName='UniformRandomProviderSupport' or
@SimpleName='RandomStreams'
- or @SimpleName='IntJumpDistances' or
@SimpleName='LongJumpDistances']"/>
+ or @SimpleName='IntJumpDistances' or @SimpleName='LongJumpDistances'
+ or @SimpleName='PhiloxSupport']"/>
<!-- Allow samplers to have only factory constructors -->
<property name="utilityClassPattern"
value="[A-Z][a-zA-Z0-9]+(Utils?|Helper|Sampler)" />
</properties>
@@ -291,6 +292,13 @@
@SimpleName='L64X256Mix']"/>
</properties>
</rule>
+ <rule ref="category/java/errorprone.xml/AvoidCatchingGenericException">
+ <properties>
+ <!-- Invocation of MethodHandle raises Throwable. -->
+ <property name="violationSuppressXPath"
+
value="./ancestor-or-self::ClassDeclaration[@SimpleName='PhiloxSupport']"/>
+ </properties>
+ </rule>
<rule ref="category/java/multithreading.xml/UseConcurrentHashMap">
<properties>