Skip to content
Permalink
Browse files
RNG-171: Reduce the memory footprint in cached boolean and int source
This change has a performance improvement on some JDKs.

Add a benchmark to compare the performance with and without the cache.
  • Loading branch information
aherbert committed Mar 20, 2022
1 parent fa1c8d5 commit d16d614cf0340c99d339a3c652e125b048904594
Showing 6 changed files with 314 additions and 104 deletions.
@@ -28,23 +28,20 @@
extends BaseProvider
implements RandomIntSource {

/** Empty boolean source. This is the location of the sign-bit after 31 right shifts on
* the boolean source. */
private static final int EMPTY_BOOL_SOURCE = 1;

/**
* Provides a bit source for booleans.
*
* <p>A cached value from a call to {@link #nextInt()}.
*/
private int booleanSource; // Initialised as 0

/**
* The bit mask of the boolean source to obtain the boolean bit.
*
* <p>The bit mask contains a single bit set. This begins at the least
* significant bit and is gradually shifted upwards until overflow to zero.
*
* <p>When zero a new boolean source should be created and the mask set to the
* least significant bit (i.e. 1).
* <p>Only stores 31-bits when full as 1 bit has already been consumed.
* The sign bit is a flag that shifts down so the source eventually equals 1
* when all bits are consumed and will trigger a refill.
*/
private int booleanBitMask; // Initialised as 0
private int booleanSource = EMPTY_BOOL_SOURCE;

/**
* Creates a new instance.
@@ -65,7 +62,6 @@ public IntProvider() {
*/
protected IntProvider(IntProvider source) {
booleanSource = source.booleanSource;
booleanBitMask = source.booleanBitMask;
}

/**
@@ -78,26 +74,21 @@ protected IntProvider(IntProvider source) {
* @since 1.3
*/
protected void resetCachedState() {
booleanSource = 0;
booleanBitMask = 0;
booleanSource = EMPTY_BOOL_SOURCE;
}

/** {@inheritDoc} */
@Override
protected byte[] getStateInternal() {
final int[] state = {booleanSource,
booleanBitMask};
return composeStateInternal(NumberFactory.makeByteArray(state),
return composeStateInternal(NumberFactory.makeByteArray(booleanSource),
super.getStateInternal());
}

/** {@inheritDoc} */
@Override
protected void setStateInternal(byte[] s) {
final byte[][] c = splitStateInternal(s, 8);
final int[] state = NumberFactory.makeIntArray(c[0]);
booleanSource = state[0];
booleanBitMask = state[1];
final byte[][] c = splitStateInternal(s, Integer.BYTES);
booleanSource = NumberFactory.makeInt(c[0]);
super.setStateInternal(c[1]);
}

@@ -110,17 +101,17 @@ public int nextInt() {
/** {@inheritDoc} */
@Override
public boolean nextBoolean() {
// Shift up. This will eventually overflow and become zero.
booleanBitMask <<= 1;
// The mask will either contain a single bit or none.
if (booleanBitMask == 0) {
// Set the least significant bit
booleanBitMask = 1;
// Get the next value
booleanSource = nextInt();
int bits = booleanSource;
if (bits == 1) {
// Refill
bits = next();
// Store a refill flag in the sign bit and the unused 31 bits, return lowest bit
booleanSource = Integer.MIN_VALUE | (bits >>> 1);
return (bits & 0x1) == 1;
}
// Return if the bit is set
return (booleanSource & booleanBitMask) != 0;
// Shift down eventually triggering refill, return current lowest bit
booleanSource = bits >>> 1;
return (bits & 0x1) == 1;
}

/** {@inheritDoc} */
@@ -28,33 +28,32 @@
extends BaseProvider
implements RandomLongSource {

/** Empty boolean source. This is the location of the sign-bit after 63 right shifts on
* the boolean source. */
private static final long EMPTY_BOOL_SOURCE = 1;
/** Empty int source. This requires a negative value as the sign-bit is used to
* trigger a refill. */
private static final long EMPTY_INT_SOURCE = -1;

/**
* Provides a bit source for booleans.
*
* <p>A cached value from a call to {@link #nextLong()}.
*/
private long booleanSource; // Initialised as 0

/**
* The bit mask of the boolean source to obtain the boolean bit.
*
* <p>The bit mask contains a single bit set. This begins at the least
* significant bit and is gradually shifted upwards until overflow to zero.
*
* <p>When zero a new boolean source should be created and the mask set to the
* least significant bit (i.e. 1).
* <p>Only stores 63-bits when full as 1 bit has already been consumed.
* The sign bit is a flag that shifts down so the source eventually equals 1
* when all bits are consumed and will trigger a refill.
*/
private long booleanBitMask; // Initialised as 0
private long booleanSource = EMPTY_BOOL_SOURCE;

/**
* Provides a source for ints.
*
* <p>A cached value from a call to {@link #nextLong()}.
* <p>A cached half-value value from a call to {@link #nextLong()}.
* The int is stored in the lower 32 bits with zeros in the upper bits.
* When empty this is set to negative to trigger a refill.
*/
private long intSource;

/** Flag to indicate an int source has been cached. */
private boolean cachedIntSource; // Initialised as false
private long intSource = EMPTY_INT_SOURCE;

/**
* Creates a new instance.
@@ -75,9 +74,7 @@ public LongProvider() {
*/
protected LongProvider(LongProvider source) {
booleanSource = source.booleanSource;
booleanBitMask = source.booleanBitMask;
intSource = source.intSource;
cachedIntSource = source.cachedIntSource;
}

/**
@@ -91,34 +88,25 @@ protected LongProvider(LongProvider source) {
* @since 1.3
*/
protected void resetCachedState() {
booleanSource = 0L;
booleanBitMask = 0L;
intSource = 0L;
cachedIntSource = false;
booleanSource = EMPTY_BOOL_SOURCE;
intSource = EMPTY_INT_SOURCE;
}

/** {@inheritDoc} */
@Override
protected byte[] getStateInternal() {
// Pack the boolean inefficiently as a long
final long[] state = {booleanSource,
booleanBitMask,
intSource,
cachedIntSource ? 1 : 0 };
final long[] state = {booleanSource, intSource};
return composeStateInternal(NumberFactory.makeByteArray(state),
super.getStateInternal());
}

/** {@inheritDoc} */
@Override
protected void setStateInternal(byte[] s) {
final byte[][] c = splitStateInternal(s, 32);
final byte[][] c = splitStateInternal(s, 2 * Long.BYTES);
final long[] state = NumberFactory.makeLongArray(c[0]);
booleanSource = state[0];
booleanBitMask = state[1];
intSource = state[2];
// Non-zero is true
cachedIntSource = state[3] != 0;
intSource = state[1];
super.setStateInternal(c[1]);
}

@@ -131,18 +119,17 @@ public long nextLong() {
/** {@inheritDoc} */
@Override
public int nextInt() {
// Directly store and use the long value as a source for ints
if (cachedIntSource) {
// Consume the cache value
cachedIntSource = false;
// Return the lower 32 bits
return NumberFactory.extractLo(intSource);
long bits = intSource;
if (bits < 0) {
// Refill
bits = next();
// Store high 32 bits, return low 32 bits
intSource = bits >>> 32;
return (int) bits;
}
// Fill the cache
cachedIntSource = true;
intSource = nextLong();
// Return the upper 32 bits
return NumberFactory.extractHi(intSource);
// Reset and return previous low bits
intSource = -1;
return (int) bits;
}

/** {@inheritDoc} */
@@ -154,17 +141,17 @@ public double nextDouble() {
/** {@inheritDoc} */
@Override
public boolean nextBoolean() {
// Shift up. This will eventually overflow and become zero.
booleanBitMask <<= 1;
// The mask will either contain a single bit or none.
if (booleanBitMask == 0) {
// Set the least significant bit
booleanBitMask = 1;
// Get the next value
booleanSource = nextLong();
long bits = booleanSource;
if (bits == 1) {
// Refill
bits = next();
// Store a refill flag in the sign bit and the unused 63 bits, return lowest bit
booleanSource = Long.MIN_VALUE | (bits >>> 1);
return (bits & 0x1) == 1;
}
// Return if the bit is set
return (booleanSource & booleanBitMask) != 0;
// Shift down eventually triggering refill, return current lowest bit
booleanSource = bits >>> 1;
return (bits & 0x1) == 1;
}

/** {@inheritDoc} */
@@ -35,14 +35,14 @@
* Tests which all {@link JumpableUniformRandomProvider} generators must pass.
*/
class JumpableProvidersParametricTest {
/** The size of the state for the IntProvider. */
private static final int INT_PROVIDER_STATE_SIZE;
/** The size of the state for the LongProvider. */
private static final int LONG_PROVIDER_STATE_SIZE;
/** The default state for the IntProvider. */
private static final byte[] INT_PROVIDER_STATE;
/** The default state for the LongProvider. */
private static final byte[] LONG_PROVIDER_STATE;

static {
INT_PROVIDER_STATE_SIZE = new State32Generator().getStateSize();
LONG_PROVIDER_STATE_SIZE = new State64Generator().getStateSize();
INT_PROVIDER_STATE = new State32Generator().getState();
LONG_PROVIDER_STATE = new State64Generator().getState();
}

/**
@@ -194,15 +194,15 @@ void testLongJumpResetsDefaultState(JumpableUniformRandomProvider generator) {
*/
private static void assertJumpResetsDefaultState(TestJumpFunction jumpFunction,
JumpableUniformRandomProvider generator) {
int stateSize;
byte[] expected;
if (generator instanceof IntProvider) {
stateSize = INT_PROVIDER_STATE_SIZE;
expected = INT_PROVIDER_STATE;
} else if (generator instanceof LongProvider) {
stateSize = LONG_PROVIDER_STATE_SIZE;
expected = LONG_PROVIDER_STATE;
} else {
throw new AssertionError("Unsupported RNG");
}
final byte[] expected = new byte[stateSize];
final int stateSize = expected.length;
for (int repeats = 0; repeats < 2; repeats++) {
// Exercise the generator.
// This calls nextInt() once so the default implementation of LongProvider
@@ -238,12 +238,13 @@ public int next() {
}

/**
* Gets the state size. This captures the state size of the IntProvider.
* Gets the default state. This captures the initial state of the IntProvider
* including the values of the cache variables.
*
* @return the state size
* @return the state
*/
int getStateSize() {
return getStateInternal().length;
byte[] getState() {
return getStateInternal();
}
}

@@ -258,12 +259,13 @@ public long next() {
}

/**
* Gets the state size. This captures the state size of the LongProvider.
* Gets the default state. This captures the initial state of the LongProvider
* including the values of the cache variables.
*
* @return the state size
*/
int getStateSize() {
return getStateInternal().length;
byte[] getState() {
return getStateInternal();
}
}

@@ -16,6 +16,7 @@
*/
package org.apache.commons.rng.core.source64;

import org.apache.commons.rng.core.util.NumberFactory;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
@@ -74,15 +75,15 @@ public long next() {

/**
* This test ensures that the call to {@link LongProvider#nextInt()} returns the
* upper and then lower 32-bits from {@link LongProvider#nextLong()}.
* lower and then upper 32-bits from {@link LongProvider#nextLong()}.
*/
@Test
void testNextInt() {
final int max = 5;
for (int i = 0; i < max; i++) {
for (int j = 0; j < max; j++) {
// Pack into upper then lower bits
final long value = (((long) i) << 32) | (j & 0xffffffffL);
// Pack into lower then upper bits
final long value = NumberFactory.makeLong(j, i);
final LongProvider provider = new FixedLongProvider(value);
Assertions.assertEquals(i, provider.nextInt(), "1st call not the upper 32-bits");
Assertions.assertEquals(j, provider.nextInt(), "2nd call not the lower 32-bits");

0 comments on commit d16d614

Please sign in to comment.