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>

Reply via email to