Skip to content
Permalink
Browse files
RNG-169: Expand primitive input seeds using a SplitMix64
This changes behaviour so that

if int == long:
  int -> array1
  long -> array2
  array1 == array2

int -> array[0] == int -> long
  • Loading branch information
aherbert committed Mar 17, 2022
1 parent d53f3da commit ef9722b5dcce2e23c250deadbed8589893d82845
Showing 7 changed files with 244 additions and 79 deletions.
@@ -27,9 +27,129 @@
* @since 1.5
*/
final class Conversions {
/**
* The fractional part of the the golden ratio, phi, scaled to 64-bits and rounded to odd.
* <pre>
* phi = (sqrt(5) - 1) / 2) * 2^64
* </pre>
* @see <a href="https://en.wikipedia.org/wiki/Golden_ratio">Golden ratio</a>
*/
private static final long GOLDEN_RATIO = 0x9e3779b97f4a7c15L;

/** No instances. */
private Conversions() {}

/**
* Perform variant 13 of David Stafford's 64-bit mix function.
* This is the mix function used in the {@link SplitMix64} RNG.
*
* <p>This is ranked first of the top 14 Stafford mixers.
*
* @param x the input value
* @return the output value
* @see <a href="http://zimbry.blogspot.com/2011/09/better-bit-mixing-improving-on.html">Better
* Bit Mixing - Improving on MurmurHash3&#39;s 64-bit Finalizer.</a>
*/
private static long stafford13(long x) {
x = (x ^ (x >>> 30)) * 0xbf58476d1ce4e5b9L;
x = (x ^ (x >>> 27)) * 0x94d049bb133111ebL;
return x ^ (x >>> 31);
}

/**
* Creates a {@code long} value from an {@code int}. The conversion
* is made as if seeding a SplitMix64 RNG and calling nextLong().
*
* @param input Input
* @return a {@code long}.
*/
static long int2long(int input) {
return stafford13(input + GOLDEN_RATIO);
}

/**
* Creates an {@code int[]} value from an {@code int}. The conversion
* is made as if seeding a SplitMix64 RNG and calling nextLong() to
* generate the sequence and filling the ints
* in little-endian order (least significant byte first).
*
* @param input Input
* @param length Array length
* @return an {@code int[]}.
*/
static int[] int2intArray(int input, int length) {
return long2intArray(input, length);
}

/**
* Creates a {@code long[]} value from an {@code int}. The conversion
* is made as if seeding a SplitMix64 RNG and calling nextLong() to
* generate the sequence.
*
* @param input Input
* @param length Array length
* @return a {@code long[]}.
*/
static long[] int2longArray(int input, int length) {
return long2longArray(input, length);
}

/**
* Creates an {@code int} value from a {@code long}. The conversion
* is made by xoring the upper and lower bits.
*
* @param input Input
* @return an {@code int}.
*/
static int long2int(long input) {
return (int) input ^ (int) (input >>> 32);
}

/**
* Creates an {@code int[]} value from a {@code long}. The conversion
* is made as if seeding a SplitMix64 RNG and calling nextLong() to
* generate the sequence and filling the ints
* in little-endian order (least significant byte first).
*
* @param input Input
* @param length Array length
* @return an {@code int[]}.
*/
static int[] long2intArray(long input, int length) {
long v = input;
final int[] output = new int[length];
// Process pairs
final int n = length & ~0x1;
for (int i = 0; i < n; i += 2) {
long x = stafford13(v += GOLDEN_RATIO);
output[i] = (int) x;
output[i + 1] = (int) (x >>> 32);
}
// Final value
if (n < length) {
output[n] = (int) stafford13(v + GOLDEN_RATIO);
}
return output;
}

/**
* Creates a {@code long[]} value from a {@code long}. The conversion
* is made as if seeding a SplitMix64 RNG and calling nextLong() to
* generate the sequence.
*
* @param input Input
* @param length Array length
* @return a {@code long}.
*/
static long[] long2longArray(long input, int length) {
long v = input;
final long[] output = new long[length];
for (int i = 0; i < length; i++) {
output[i] = stafford13(v += GOLDEN_RATIO);
}
return output;
}

/**
* Creates an {@code int} value from a sequence of bytes. The conversion
* is made as if converting to a {@code int[]} array by filling the ints
@@ -16,8 +16,6 @@
*/
package org.apache.commons.rng.simple.internal;

import org.apache.commons.rng.core.util.NumberFactory;

/**
* Converts a {@code Integer} to an {@code Long}.
*
@@ -27,7 +25,6 @@ public class Int2Long implements SeedConverter<Integer, Long> {
/** {@inheritDoc} */
@Override
public Long convert(Integer seed) {
final int s = seed;
return NumberFactory.makeLong(s, ~s);
return Conversions.int2long(seed);
}
}
@@ -16,11 +16,9 @@
*/
package org.apache.commons.rng.simple.internal;

import org.apache.commons.rng.core.source64.SplitMix64;
import org.apache.commons.rng.core.util.NumberFactory;

/**
* Uses a {@code long} value to seed a {@link SplitMix64} RNG and
* Uses a {@code long} value to seed a
* {@link org.apache.commons.rng.core.source64.SplitMix64 SplitMix64} RNG and
* create a {@code int[]} with the requested number of random
* values.
*
@@ -40,7 +38,7 @@ public Long2IntArray(int size) {
/** {@inheritDoc} */
@Override
public int[] convert(Long seed) {
return convertSeed(seed, size);
return Conversions.long2intArray(seed, size);
}

/**
@@ -50,30 +48,6 @@ public int[] convert(Long seed) {
*/
@Override
public int[] convert(Long seed, int outputSize) {
return convertSeed(seed, outputSize);
}

/**
* Convert the seed.
*
* @param seed Input seed.
* @param size Output array size.
* @return the converted seed.
*/
private static int[] convertSeed(Long seed, int size) {
final int[] out = new int[size];
final SplitMix64 rng = new SplitMix64(seed);
// Fill pairs of ints from a long.
// The array is filled from the end towards the start.
for (int i = size - 1; i > 0; i -= 2) {
final long v = rng.nextLong();
out[i] = NumberFactory.extractHi(v);
out[i - 1] = NumberFactory.extractLo(v);
}
// An odd size requires a final single int at the start
if ((size & 1) == 1) {
out[0] = NumberFactory.extractHi(rng.nextLong());
}
return out;
return Conversions.long2intArray(seed, outputSize);
}
}
@@ -16,10 +16,9 @@
*/
package org.apache.commons.rng.simple.internal;

import org.apache.commons.rng.core.source64.SplitMix64;

/**
* Uses a {@code Long} value to seed a {@link SplitMix64} RNG and
* Uses a {@code Long} value to seed a
* {@link org.apache.commons.rng.core.source64.SplitMix64 SplitMix64} RNG and
* create a {@code long[]} with the requested number of random
* values.
*
@@ -40,7 +39,7 @@ public Long2LongArray(int size) {
/** {@inheritDoc} */
@Override
public long[] convert(Long seed) {
return convertSeed(seed, size);
return Conversions.long2longArray(seed, size);
}

/**
@@ -50,23 +49,6 @@ public long[] convert(Long seed) {
*/
@Override
public long[] convert(Long seed, int outputSize) {
return convertSeed(seed, outputSize);
}

/**
* Convert the seed.
*
* @param seed Input seed.
* @param size Output array size.
* @return the converted seed.
*/
private static long[] convertSeed(Long seed, int size) {
final long[] out = new long[size];
final SplitMix64 rng = new SplitMix64(seed);
for (int i = 0; i < size; i++) {
out[i] = rng.nextLong();
}

return out;
return Conversions.long2longArray(seed, outputSize);
}
}
@@ -54,7 +54,7 @@ protected Integer convert(Integer seed, int size) {
}
@Override
protected Integer convert(Long seed, int size) {
return LONG_TO_INT.convert(seed);
return Conversions.long2int(seed);
}
@Override
protected Integer convert(int[] seed, int size) {
@@ -77,7 +77,7 @@ public Long createSeed(int size) {
}
@Override
protected Long convert(Integer seed, int size) {
return INT_TO_LONG.convert(seed);
return Conversions.int2long(seed);
}
@Override
protected Long convert(Long seed, int size) {
@@ -106,11 +106,11 @@ public int[] createSeed(int size) {
}
@Override
protected int[] convert(Integer seed, int size) {
return LONG_TO_INT_ARRAY.convert(INT_TO_LONG.convert(seed), size);
return Conversions.int2intArray(seed, size);
}
@Override
protected int[] convert(Long seed, int size) {
return LONG_TO_INT_ARRAY.convert(seed, size);
return Conversions.long2intArray(seed, size);
}
@Override
protected int[] convert(int[] seed, int size) {
@@ -139,11 +139,11 @@ public long[] createSeed(int size) {
}
@Override
protected long[] convert(Integer seed, int size) {
return LONG_TO_LONG_ARRAY.convert(INT_TO_LONG.convert(seed), size);
return Conversions.int2longArray(seed, size);
}
@Override
protected long[] convert(Long seed, int size) {
return LONG_TO_LONG_ARRAY.convert(seed, size);
return Conversions.long2longArray(seed, size);
}
@Override
protected long[] convert(int[] seed, int size) {
@@ -169,12 +169,6 @@ protected long[] convert(byte[] seed, int size) {
private static final int RANDOM_SEED_ARRAY_SIZE = 128;
/** Convert {@code Long} to {@code Integer}. */
private static final Long2Int LONG_TO_INT = new Long2Int();
/** Convert {@code Integer} to {@code Long}. */
private static final Int2Long INT_TO_LONG = new Int2Long();
/** Convert {@code Long} to {@code int[]}. */
private static final Long2IntArray LONG_TO_INT_ARRAY = new Long2IntArray(0);
/** Convert {@code Long} to {@code long[]}. */
private static final Long2LongArray LONG_TO_LONG_ARRAY = new Long2LongArray(0);
/** Convert {@code long[]} to {@code Long}. */
private static final LongArray2Long LONG_ARRAY_TO_LONG = new LongArray2Long();
/** Convert {@code int[]} to {@code Integer}. */
@@ -22,7 +22,11 @@
import java.util.Arrays;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.IntStream;
import java.util.stream.LongStream;
import org.apache.commons.rng.core.source64.SplitMix64;
import org.apache.commons.rng.core.util.NumberFactory;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

@@ -48,6 +52,72 @@ static IntStream getIntLengths() {
return IntStream.rangeClosed(0, (Long.BYTES / Integer.BYTES) * 2);
}

@RepeatedTest(value = 5)
void testInt2Long() {
final int v = ThreadLocalRandom.current().nextInt();
Assertions.assertEquals(new SplitMix64(v).nextLong(), Conversions.int2long(v));
}

@RepeatedTest(value = 5)
void testInt2IntArray() {
final int v = ThreadLocalRandom.current().nextInt();
getIntLengths().forEach(len -> {
Assertions.assertArrayEquals(Conversions.long2intArray(v, len),
Conversions.int2intArray(v, len));
});
}

@RepeatedTest(value = 5)
void testInt2LongArray() {
final int v = ThreadLocalRandom.current().nextInt();
getIntLengths().forEach(len -> {
final long[] a = Conversions.int2longArray(v, len);
Assertions.assertArrayEquals(Conversions.long2longArray(v, len), a);
if (len != 0) {
// Special case of expansion to length 1
// Expandion is done by mixing
Assertions.assertEquals(Conversions.int2long(v), a[0]);
}
});
}

@RepeatedTest(value = 5)
void testLong2Int() {
final long v = ThreadLocalRandom.current().nextLong();
Assertions.assertEquals(NumberFactory.makeInt(v), Conversions.long2int(v));
}

@RepeatedTest(value = 5)
void testLong2IntArray() {
final long v = ThreadLocalRandom.current().nextLong();
getIntLengths().forEach(len -> {
final int longs = SeedUtils.longSizeFromIntSize(len);
// Little-endian conversion
final ByteBuffer bb = ByteBuffer.allocate(longs * Long.BYTES).order(ByteOrder.LITTLE_ENDIAN);
LongStream.generate(new SplitMix64(v)::nextLong).limit(longs).forEach(bb::putLong);
bb.clear();
final int[] expected = new int[len];
for (int i = 0; i < len; i++) {
expected[i] = bb.getInt();
}
Assertions.assertArrayEquals(expected,
Conversions.long2intArray(v, len));

// Note:
// long -> int[] position[0] != long -> int
// Reduction is done by folding upper and lower using xor
});
}

@RepeatedTest(value = 5)
void testLong2LongArray() {
final long v = ThreadLocalRandom.current().nextLong();
getIntLengths().forEach(len -> {
Assertions.assertArrayEquals(LongStream.generate(new SplitMix64(v)::nextLong).limit(len).toArray(),
Conversions.long2longArray(v, len));
});
}

@ParameterizedTest
@MethodSource(value = {"getIntLengths"})
void testByteArray2Int(int bytes) {

0 comments on commit ef9722b

Please sign in to comment.