Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions commons-rng-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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));
}
}
}
}
2 changes: 2 additions & 0 deletions src/conf/checkstyle/checkstyle-suppressions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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"/>
Expand Down
10 changes: 9 additions & 1 deletion src/conf/pmd/pmd-ruleset.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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>
Expand Down
Loading