Skip to content

Commit d73cbc4

Browse files
committed
RNG-191: Dynamically call Math multiply high methods
Adds unsignedMultiplyHigh methods potentially using native 128-bit multiplication to the Philox4x64 generator.
1 parent e84e1d9 commit d73cbc4

7 files changed

Lines changed: 279 additions & 4 deletions

File tree

commons-rng-core/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@
5555
<!-- Change from commons-parent of 1.0 as some illegal state cases cannot be reached -->
5656
<commons.jacoco.instructionRatio>0.99</commons.jacoco.instructionRatio>
5757
<commons.jacoco.lineRatio>0.99</commons.jacoco.lineRatio>
58+
<!-- Change from commons-parent of 1.0 as method handles to JDK Math cannot always be executed -->
59+
<commons.jacoco.methodRatio>0.99</commons.jacoco.methodRatio>
60+
<commons.jacoco.complexityRatio>0.99</commons.jacoco.complexityRatio>
61+
62+
<!-- Disable to allow use of MethodHandle to higher JDK version methods. -->
63+
<animal.sniffer.skip>true</animal.sniffer.skip>
5864
</properties>
5965

6066
<dependencies>

commons-rng-core/src/main/java/org/apache/commons/rng/core/source64/Philox4x64.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -239,9 +239,9 @@ private void rand10() {
239239
*/
240240
private static void singleRound(long[] counter, long key0, long key1) {
241241
final long lo0 = PHILOX_M0 * counter[0];
242-
final long hi0 = LXMSupport.unsignedMultiplyHigh(PHILOX_M0, counter[0]);
242+
final long hi0 = PhiloxSupport.unsignedMultiplyHigh(PHILOX_M0, counter[0]);
243243
final long lo1 = PHILOX_M1 * counter[2];
244-
final long hi1 = LXMSupport.unsignedMultiplyHigh(PHILOX_M1, counter[2]);
244+
final long hi1 = PhiloxSupport.unsignedMultiplyHigh(PHILOX_M1, counter[2]);
245245

246246
counter[0] = hi1 ^ counter[1] ^ key0;
247247
counter[1] = lo1;
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.commons.rng.core.source64;
18+
19+
import java.lang.invoke.MethodHandle;
20+
import java.lang.invoke.MethodHandles;
21+
import java.lang.invoke.MethodType;
22+
import java.util.function.LongBinaryOperator;
23+
import java.util.stream.Stream;
24+
25+
/**
26+
* Utility support for the Philox family of generators.
27+
*
28+
* <p>Contains methods that use the {@code java.lang.invoke} package to call
29+
* {@code java.lang.Math} functions for computing the high part of the 128-bit
30+
* result of a multiply of two 64-bit longs. These methods may be supported
31+
* by intrinsic calls to native operations if supported on the platform for
32+
* a significant performance gain.
33+
*
34+
* <p>Note
35+
*
36+
* <p>This class is used specifically in the {@link Philox4x64} generator which
37+
* has a state update cycle which is performance dependent on the multiply
38+
* of two unsigned long values. Other classes which use unsigned multiply
39+
* and are not performance dependent on the method do not use this implementation
40+
* (for example the LXM family of generators). This allows the multiply method
41+
* to be adapted to the usage of {@link Philox4x64} which always has the first
42+
* argument as a negative constant.
43+
*
44+
* @since 1.7
45+
*/
46+
final class PhiloxSupport {
47+
/**
48+
* Method to compute unsigned multiply high. Uses:
49+
* <ul>
50+
* <li>{@code java.lang.Math.unsignedMultiplyHigh} if Java 18
51+
* <li>{@code java.lang.Math.multiplyHigh} if Java 9
52+
* <li>otherwise a default implementation.
53+
* </ul>
54+
*/
55+
private static final LongBinaryOperator UNSIGNED_MULTIPLY_HIGH;
56+
57+
static {
58+
// Note:
59+
// This uses the public lookup mechanism for static methods to find methods
60+
// added to java.lang.Math since java 8 to make them available in java 8.
61+
// For simplicity the lookup is always attempted rather than checking the
62+
// the java version from System.getProperty("java.version").
63+
final LongBinaryOperator op1 = getMathUnsignedMultiplyHigh();
64+
final LongBinaryOperator op2 = getMathMultiplyHigh();
65+
UNSIGNED_MULTIPLY_HIGH = Stream.of(op1, op2)
66+
.filter(PhiloxSupport::testUnsignedMultiplyHigh)
67+
.findFirst()
68+
.orElse(LXMSupport::unsignedMultiplyHigh);
69+
}
70+
71+
/** No instances. */
72+
private PhiloxSupport() {}
73+
74+
/**
75+
* Gets a method to compute the high 64-bits of an unsigned 64-bit multiplication
76+
* using the Math unsignedMultiplyHigh method from JDK 18.
77+
*
78+
* @return the method, or null
79+
*/
80+
private static LongBinaryOperator getMathUnsignedMultiplyHigh() {
81+
try {
82+
// JDK 18 method
83+
final MethodHandle mh = getMathMethod("unsignedMultiplyHigh");
84+
return (a, b) -> {
85+
try {
86+
return (long) mh.invokeExact(a, b);
87+
} catch (Throwable ignored) {
88+
throw new IllegalStateException("Cannot invoke Math.unsignedMultiplyHigh");
89+
}
90+
};
91+
} catch (NoSuchMethodException | IllegalAccessException ignored) {
92+
return null;
93+
}
94+
}
95+
96+
/**
97+
* Gets a method to compute the high 64-bits of an unsigned 64-bit multiplication
98+
* using the Math multiplyHigh method from JDK 9.
99+
*
100+
* @return the method, or null
101+
*/
102+
private static LongBinaryOperator getMathMultiplyHigh() {
103+
try {
104+
// JDK 9 method
105+
final MethodHandle mh = getMathMethod("multiplyHigh");
106+
return (a, b) -> {
107+
try {
108+
// Correct signed result to unsigned.
109+
// Assume a is negative, but use sign bit to check b is negative.
110+
return (long) mh.invokeExact(a, b) + b + ((b >> 63) & a);
111+
} catch (Throwable ignored) {
112+
throw new IllegalStateException("Cannot invoke Math.multiplyHigh");
113+
}
114+
};
115+
} catch (NoSuchMethodException | IllegalAccessException ignored) {
116+
return null;
117+
}
118+
}
119+
120+
/**
121+
* Gets the named method from the {@link Math} class.
122+
*
123+
* <p>The look-up assumes the named method accepts two long arguments and returns
124+
* a long.
125+
*
126+
* @param methodName Method name.
127+
* @return the method
128+
* @throws NoSuchMethodException if the method does not exist
129+
* @throws IllegalAccessException if the method cannot be accessed
130+
*/
131+
static MethodHandle getMathMethod(String methodName) throws NoSuchMethodException, IllegalAccessException {
132+
return MethodHandles.publicLookup()
133+
.findStatic(Math.class,
134+
methodName,
135+
MethodType.methodType(long.class, long.class, long.class));
136+
}
137+
138+
/**
139+
* Test the implementation of unsigned multiply high.
140+
* It is assumed the invocation of the method may raise an {@link IllegalStateException}
141+
* if it cannot be invoked.
142+
*
143+
* @param op Method implementation.
144+
* @return True if the method can be called to generate the expected result
145+
*/
146+
static boolean testUnsignedMultiplyHigh(LongBinaryOperator op) {
147+
try {
148+
// Test with a signed input to the multiplication.
149+
// The result is: (1L << 63) * 2 == 1LL << 64
150+
return op != null && op.applyAsLong(Long.MIN_VALUE, 2L) == 1;
151+
} catch (IllegalStateException ignored) {
152+
return false;
153+
}
154+
}
155+
156+
/**
157+
* Multiply the two values as if unsigned 64-bit longs to produce the high 64-bits
158+
* of the 128-bit unsigned result. The first argument is assumed to be negative.
159+
*
160+
* <p>This method uses a {@link MethodHandle} to call Java functions added since
161+
* Java 8 to the {@link Math} class:
162+
* <ul>
163+
* <li>{@code java.lang.Math.unsignedMultiplyHigh} if Java 18
164+
* <li>{@code java.lang.Math.multiplyHigh} if Java 9
165+
* <li>otherwise a default implementation.
166+
* </ul>
167+
*
168+
* <p><strong>Warning</strong>
169+
*
170+
* <p>For performance reasons this method assumes the first argument is negative.
171+
* This allows some operations to be dropped if running on Java 9 to 17.
172+
*
173+
* @param value1 the first value (must be negative)
174+
* @param value2 the second value
175+
* @return the high 64-bits of the 128-bit result
176+
*/
177+
static long unsignedMultiplyHigh(long value1, long value2) {
178+
return UNSIGNED_MULTIPLY_HIGH.applyAsLong(value1, value2);
179+
}
180+
}

commons-rng-core/src/test/java/org/apache/commons/rng/core/source64/LXMSupportTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ void testUnsignedMultiplyHigh() {
108108
}
109109
}
110110

111-
private static void assertMultiplyHigh(long v1, long v2, long hi) {
111+
static void assertMultiplyHigh(long v1, long v2, long hi) {
112112
final BigInteger bi1 = toUnsignedBigInteger(v1);
113113
final BigInteger bi2 = toUnsignedBigInteger(v2);
114114
final BigInteger expected = bi1.multiply(bi2);
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.commons.rng.core.source64;
18+
19+
import java.util.SplittableRandom;
20+
import org.junit.jupiter.api.Assertions;
21+
import org.junit.jupiter.api.Test;
22+
23+
/**
24+
* Tests for {@link PhiloxSupport}.
25+
*/
26+
class PhiloxSupportTest {
27+
@Test
28+
void testGetMathMethod() throws NoSuchMethodException, IllegalAccessException {
29+
// Java 8 method: Math.addExact(long, long)
30+
Assertions.assertNotNull(PhiloxSupport.getMathMethod("addExact"));
31+
Assertions.assertThrows(NoSuchMethodException.class, () -> PhiloxSupport.getMathMethod("foo"));
32+
}
33+
34+
@Test
35+
void testTestUnsignedMultiplyHigh() {
36+
Assertions.assertTrue(PhiloxSupport.testUnsignedMultiplyHigh(LXMSupport::unsignedMultiplyHigh));
37+
// Test all code paths
38+
Assertions.assertFalse(PhiloxSupport.testUnsignedMultiplyHigh(null), "Null operator");
39+
Assertions.assertFalse(PhiloxSupport.testUnsignedMultiplyHigh((a, b) -> 0), "Invalid multiply operator");
40+
Assertions.assertFalse(PhiloxSupport.testUnsignedMultiplyHigh((a, b) -> {
41+
throw new IllegalStateException();
42+
}), "Illegal call to operator");
43+
}
44+
45+
@Test
46+
void testUnsignedMultiplyHighEdgeCases() {
47+
final long[] values = {
48+
-1, 0, 1, Long.MAX_VALUE, Long.MIN_VALUE,
49+
0xffL, 0xff00L, 0xff0000L, 0xff000000L,
50+
0xff00000000L, 0xff0000000000L, 0xff000000000000L, 0xff000000000000L,
51+
0xffffL, 0xffff0000L, 0xffff00000000L, 0xffff000000000000L,
52+
0xffffffffL, 0xffffffff00000000L,
53+
// Philox 4x64 multiplication constants
54+
0xD2E7470EE14C6C93L, 0xCA5A826395121157L,
55+
};
56+
57+
for (final long v1 : values) {
58+
// Must be odd
59+
if (v1 >= 0) {
60+
continue;
61+
}
62+
for (final long v2 : values) {
63+
LXMSupportTest.assertMultiplyHigh(v1, v2, PhiloxSupport.unsignedMultiplyHigh(v1, v2));
64+
}
65+
}
66+
}
67+
68+
@Test
69+
void testUnsignedMultiplyHigh() {
70+
final long[] values = new SplittableRandom().longs(100).toArray();
71+
for (long v1 : values) {
72+
// Must be odd
73+
v1 |= Long.MIN_VALUE;
74+
for (final long v2 : values) {
75+
LXMSupportTest.assertMultiplyHigh(v1, v2, PhiloxSupport.unsignedMultiplyHigh(v1, v2));
76+
}
77+
}
78+
}
79+
}

src/conf/checkstyle/checkstyle-suppressions.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
<suppress checks="UnnecessaryParentheses" files=".*stress[/\\]StressTestCommand\.java$" lines="696" />
2727
<!-- Special to allow withUniformRandomProvider to act as a constructor. -->
2828
<suppress checks="HiddenField" files=".*Sampler\.java$" message="'rng' hides a field." />
29+
<!-- Invocation of MethodHandle raises Throwable. -->
30+
<suppress checks="IllegalCatch" files="source64[\\/]PhiloxSupport\.java$"/>
2931
<!-- Methods have the names from the Spliterator interface that is implemented by child classes.
3032
Classes are package-private and should not require documentation. -->
3133
<suppress checks="MissingJavadocMethod" files="[\\/]UniformRandomProviderSupport\.java$" lines="479-484"/>

src/conf/pmd/pmd-ruleset.xml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,8 @@
120120
or @SimpleName='Coordinates' or @SimpleName='Hex' or @SimpleName='SpecialMath'
121121
or @SimpleName='Conversions' or @SimpleName='MixFunctions' or @SimpleName='LXMSupport'
122122
or @SimpleName='UniformRandomProviderSupport' or @SimpleName='RandomStreams'
123-
or @SimpleName='IntJumpDistances' or @SimpleName='LongJumpDistances']"/>
123+
or @SimpleName='IntJumpDistances' or @SimpleName='LongJumpDistances'
124+
or @SimpleName='PhiloxSupport']"/>
124125
<!-- Allow samplers to have only factory constructors -->
125126
<property name="utilityClassPattern" value="[A-Z][a-zA-Z0-9]+(Utils?|Helper|Sampler)" />
126127
</properties>
@@ -291,6 +292,13 @@
291292
@SimpleName='L64X256Mix']"/>
292293
</properties>
293294
</rule>
295+
<rule ref="category/java/errorprone.xml/AvoidCatchingGenericException">
296+
<properties>
297+
<!-- Invocation of MethodHandle raises Throwable. -->
298+
<property name="violationSuppressXPath"
299+
value="./ancestor-or-self::ClassDeclaration[@SimpleName='PhiloxSupport']"/>
300+
</properties>
301+
</rule>
294302

295303
<rule ref="category/java/multithreading.xml/UseConcurrentHashMap">
296304
<properties>

0 commit comments

Comments
 (0)