blob: 921c003a2314ce78e616ee56355258871d6c3a32 [file] [log] [blame]
/*
* 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.examples.jmh;
import org.apache.commons.rng.UniformRandomProvider;
import org.apache.commons.rng.core.source32.IntProvider;
import org.apache.commons.rng.sampling.PermutationSampler;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OperationsPerInvocation;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
/**
* Executes benchmark to compare the speed of random number generators to create
* an int value in a range.
*/
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@State(Scope.Benchmark)
@Fork(value = 1, jvmArgs = { "-server", "-Xms128M", "-Xmx128M" })
public class RngNextIntInRangeBenchmark {
/** The value. Must NOT be final to prevent JVM optimisation! */
private int intValue;
/**
* The upper range for the {@code int} generation.
*/
@State(Scope.Benchmark)
public static class IntRange {
/**
* The upper range for the {@code int} generation.
*
* <p>Note that the while loop uses a rejection algorithm. From the Javadoc for
* java.util.Random:</p>
*
* <pre>
* "The probability of a value being rejected depends on n. The
* worst case is n=2^30+1, for which the probability of a reject is 1/2,
* and the expected number of iterations before the loop terminates is 2."
* </pre>
*/
@Param({"16", "17", "256", "257", "4096", "4097",
// Worst case power-of-2: (1 << 30)
"1073741824",
// Worst case: (1 << 30) + 1
"1073741825", })
private int n;
/**
* Gets the upper bound {@code n}.
*
* @return the upper bound
*/
public int getN() {
return n;
}
}
/**
* The data used for the shuffle benchmark.
*/
@State(Scope.Benchmark)
public static class IntData {
/**
* The size of the data.
*/
@Param({ "4", "16", "256", "4096", "16384" })
private int size;
/** The data. */
private int[] data;
/**
* Gets the data.
*
* @return the data
*/
public int[] getData() {
return data;
}
/**
* Create the data.
*/
@Setup
public void setup() {
data = PermutationSampler.natural(size);
}
}
/**
* The source generator.
*/
@State(Scope.Benchmark)
public static class Source {
/**
* The name of the generator.
*/
@Param({ "jdk", "jdkPow2", "lemire", "lemirePow2", "lemire31", "lemire31Pow2"})
private String name;
/** The random generator. */
private UniformRandomProvider rng;
/**
* Gets the random generator.
*
* @return the generator
*/
public UniformRandomProvider getRng() {
return rng;
}
/** Create the generator. */
@Setup
public void setup() {
final long seed = ThreadLocalRandom.current().nextLong();
if ("jdk".equals(name)) {
rng = new JDK(seed);
} else if ("jdkPow2".equals(name)) {
rng = new JDKPow2(seed);
} else if ("lemire".equals(name)) {
rng = new Lemire(seed);
} else if ("lemirePow2".equals(name)) {
rng = new LemirePow2(seed);
} else if ("lemire31".equals(name)) {
rng = new Lemire31(seed);
} else if ("lemire31Pow2".equals(name)) {
rng = new Lemire31Pow2(seed);
}
}
}
/**
* Implement the SplitMix algorithm from {@link java.util.SplittableRandom
* SplittableRandom} to output 32-bit int values.
*
* <p>This is a base generator to test nextInt(int) methods.
*/
abstract static class SplitMix32 extends IntProvider {
/**
* The golden ratio, phi, scaled to 64-bits and rounded to odd.
*/
private static final long GOLDEN_RATIO = 0x9e3779b97f4a7c15L;
/** The state. */
protected long state;
/**
* Create a new instance.
*
* @param seed the seed
*/
SplitMix32(long seed) {
state = seed;
}
@Override
public int next() {
long key = state += GOLDEN_RATIO;
// 32 high bits of Stafford variant 4 mix64 function as int:
// http://zimbry.blogspot.com/2011/09/better-bit-mixing-improving-on.html
key = (key ^ (key >>> 33)) * 0x62a9d9ed799705f5L;
return (int) (((key ^ (key >>> 28)) * 0xcb24d0a5c88c35b3L) >>> 32);
}
/**
* Check the value is strictly positive.
*
* @param n the value
*/
void checkStrictlyPositive(int n) {
if (n <= 0) {
throw new IllegalArgumentException("not strictly positive: " + n);
}
}
}
/**
* Implement the nextInt(int) method of the JDK excluding the case for a power-of-2 range.
*/
static class JDK extends SplitMix32 {
/**
* Create a new instance.
*
* @param seed the seed
*/
JDK(long seed) {
super(seed);
}
@Override
public int nextInt(int n) {
checkStrictlyPositive(n);
int bits;
int val;
do {
bits = next() >>> 1;
val = bits % n;
} while (bits - val + n - 1 < 0);
return val;
}
}
/**
* Implement the nextInt(int) method of the JDK with a case for a power-of-2 range.
*/
static class JDKPow2 extends SplitMix32 {
/**
* Create a new instance.
*
* @param seed the seed
*/
JDKPow2(long seed) {
super(seed);
}
@Override
public int nextInt(int n) {
checkStrictlyPositive(n);
final int nm1 = n - 1;
if ((n & nm1) == 0) {
// Power of 2
return next() & nm1;
}
int bits;
int val;
do {
bits = next() >>> 1;
val = bits % n;
} while (bits - val + nm1 < 0);
return val;
}
}
/**
* Implement the nextInt(int) method of Lemire (2019).
*
* @see <a href="https://arxiv.org/abs/1805.10941SplittableRandom"> Lemire
* (2019): Fast Random Integer Generation in an Interval</a>
*/
static class Lemire extends SplitMix32 {
/** 2^32. */
static final long POW_32 = 1L << 32;
/**
* Create a new instance.
*
* @param seed the seed
*/
Lemire(long seed) {
super(seed);
}
@Override
public int nextInt(int n) {
checkStrictlyPositive(n);
long m = (next() & 0xffffffffL) * n;
long l = m & 0xffffffffL;
if (l < n) {
// 2^32 % n
final long t = POW_32 % n;
while (l < t) {
m = (next() & 0xffffffffL) * n;
l = m & 0xffffffffL;
}
}
return (int) (m >>> 32);
}
}
/**
* Implement the nextInt(int) method of Lemire (2019) with a case for a power-of-2 range.
*/
static class LemirePow2 extends SplitMix32 {
/** 2^32. */
static final long POW_32 = 1L << 32;
/**
* Create a new instance.
*
* @param seed the seed
*/
LemirePow2(long seed) {
super(seed);
}
@Override
public int nextInt(int n) {
checkStrictlyPositive(n);
final int nm1 = n - 1;
if ((n & nm1) == 0) {
// Power of 2
return next() & nm1;
}
long m = (next() & 0xffffffffL) * n;
long l = m & 0xffffffffL;
if (l < n) {
// 2^32 % n
final long t = POW_32 % n;
while (l < t) {
m = (next() & 0xffffffffL) * n;
l = m & 0xffffffffL;
}
}
return (int) (m >>> 32);
}
}
/**
* Implement the nextInt(int) method of Lemire (2019) modified to 31-bit arithmetic to use
* an int modulus operation.
*/
static class Lemire31 extends SplitMix32 {
/** 2^32. */
static final long POW_32 = 1L << 32;
/**
* Create a new instance.
*
* @param seed the seed
*/
Lemire31(long seed) {
super(seed);
}
@Override
public int nextInt(int n) {
checkStrictlyPositive(n);
long m = (nextInt() & 0x7fffffffL) * n;
long l = m & 0x7fffffffL;
if (l < n) {
// 2^31 % n
final long t = (Integer.MIN_VALUE - n) % n;
while (l < t) {
m = (nextInt() & 0x7fffffffL) * n;
l = m & 0x7fffffffL;
}
}
return (int) (m >>> 31);
}
}
/**
* Implement the nextInt(int) method of Lemire (2019) modified to 31-bit arithmetic to use
* an int modulus operation, with a case for a power-of-2 range.
*/
static class Lemire31Pow2 extends SplitMix32 {
/** 2^32. */
static final long POW_32 = 1L << 32;
/**
* Create a new instance.
*
* @param seed the seed
*/
Lemire31Pow2(long seed) {
super(seed);
}
@Override
public int nextInt(int n) {
checkStrictlyPositive(n);
final int nm1 = n - 1;
if ((n & nm1) == 0) {
// Power of 2
return next() & nm1;
}
long m = (nextInt() & 0x7fffffffL) * n;
long l = m & 0x7fffffffL;
if (l < n) {
// 2^31 % n
final long t = (Integer.MIN_VALUE - n) % n;
while (l < t) {
m = (nextInt() & 0x7fffffffL) * n;
l = m & 0x7fffffffL;
}
}
return (int) (m >>> 31);
}
}
/**
* Baseline for a JMH method call returning an {@code int}.
*
* @return the value
*/
@Benchmark
public int baselineInt() {
return intValue;
}
/**
* Exercise the {@link UniformRandomProvider#nextInt()} method.
*
* @param range the range
* @param source Source of randomness.
* @return the int
*/
@Benchmark
public int nextIntN(IntRange range, Source source) {
return source.getRng().nextInt(range.getN());
}
/**
* Exercise the {@link UniformRandomProvider#nextInt()} method in a loop.
*
* @param range the range
* @param source Source of randomness.
* @return the int
*/
@Benchmark
@OperationsPerInvocation(65_536)
public int nextIntNloop65536(IntRange range, Source source) {
int sum = 0;
for (int i = 0; i < 65_536; i++) {
sum += source.getRng().nextInt(range.getN());
}
return sum;
}
/**
* Exercise the {@link UniformRandomProvider#nextInt(int)} method by shuffling
* data.
*
* @param data the data
* @param source Source of randomness.
* @return the shuffle data
*/
@Benchmark
public int[] shuffle(IntData data, Source source) {
final int[] array = data.getData();
shuffle(array, source.getRng());
return array;
}
/**
* Perform a Fischer-Yates shuffle.
*
* @param array the array
* @param rng the random generator
*/
private static void shuffle(int[] array, UniformRandomProvider rng) {
for (int i = array.length - 1; i > 0; i--) {
// Swap index with any position down to 0
final int j = rng.nextInt(i);
final int tmp = array[j];
array[j] = array[i];
array[i] = tmp;
}
}
/**
* Exercise the {@link UniformRandomProvider#nextInt(int)} method by creating
* random indices for shuffling data.
*
* @param data the data
* @param source Source of randomness.
* @return the sum
*/
@Benchmark
public int pseudoShuffle(IntData data, Source source) {
int sum = 0;
for (int i = data.getData().length - 1; i > 0; i--) {
sum += source.getRng().nextInt(i);
}
return sum;
}
}