diff --git a/commons-rng-core/pom.xml b/commons-rng-core/pom.xml index f0de437e9..c6fc94fcc 100644 --- a/commons-rng-core/pom.xml +++ b/commons-rng-core/pom.xml @@ -55,6 +55,12 @@ 0.99 0.99 + + 0.99 + 0.99 + + + true 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 813ef02c3..8e92925de 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 @@ private void rand10() { */ 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 000000000..35a2a7bb5 --- /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. + * + *

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. + * + *

Note + * + *

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: + *

+ */ + 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. + * + *

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. + * + *

This method uses a {@link MethodHandle} to call Java functions added since + * Java 8 to the {@link Math} class: + *

+ * + *

Warning + * + *

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 8a8138cf5..6da9258cd 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 @@ void testUnsignedMultiplyHigh() { } } - 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 000000000..cbccb85a8 --- /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 f9c106c7d..fe523ec80 100644 --- a/src/conf/checkstyle/checkstyle-suppressions.xml +++ b/src/conf/checkstyle/checkstyle-suppressions.xml @@ -26,6 +26,8 @@ + + diff --git a/src/conf/pmd/pmd-ruleset.xml b/src/conf/pmd/pmd-ruleset.xml index 94d113a70..a716e49c8 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']"/> @@ -291,6 +292,13 @@ @SimpleName='L64X256Mix']"/> + + + + + +