Skip to content

Commit

Permalink
RNG-169: Avoid intermediate arrays during conversions
Browse files Browse the repository at this point in the history
The following conversions now avoid intermediate arrays:

byte[] -> int
byte[] -> long
int[] -> long

Behaviour change:

int[] is now converted as if to long[] then to long. This avoids loss of
bits and changes the possible output seeds from 2^32 to 2^64.
  • Loading branch information
aherbert committed Mar 17, 2022
1 parent ca6f140 commit d53f3da
Show file tree
Hide file tree
Showing 6 changed files with 620 additions and 8 deletions.
@@ -0,0 +1,107 @@
/*
* 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.simple.internal;

/**
* Performs seed conversions.
*
* <p>Note: Legacy converters from version 1.0 use instances of
* the {@link SeedConverter} interface. Instances are no longer
* required as no state is used during conversion and converters
* can use static methods.
*
* @since 1.5
*/
final class Conversions {
/** No instances. */
private Conversions() {}

/**
* 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
* in little-endian order (least significant byte first), then combining
* all the ints with a xor operation.
*
* @param input Input bytes
* @return an {@code int}.
*/
static int byteArray2Int(byte[] input) {
int output = 0;

final int n = input.length;
// xor in the bits to an int in little-endian order
for (int i = 0; i < n; i++) {
// i = byte index
// i >> 2 = integer index
// i & 0x3 = byte number in the integer [0, 3]
// (i & 0x3) << 3 = little-endian byte shift to the integer {0, 8, 16, 24}
output ^= (input[i] & 0xff) << ((i & 0x3) << 3);
}

return output;
}

/**
* Creates a {@code long} value from a sequence of bytes. The conversion
* is made as if converting to a {@code long[]} array by filling the longs
* in little-endian order (least significant byte first), then combining
* all the longs with a xor operation.
*
* @param input Input bytes
* @return a {@code long}.
*/
static long byteArray2Long(byte[] input) {
long output = 0;

final int n = input.length;
// xor in the bits to a long in little-endian order
for (int i = 0; i < n; i++) {
// i = byte index
// i >> 3 = long index
// i & 0x7 = byte number in the long [0, 7]
// (i & 0x7) << 3 = little-endian byte shift to the long {0, 8, 16, 24, 32, 36, 40, 48, 56}
output ^= (input[i] & 0xffL) << ((i & 0x7) << 3);
}

return output;
}

/**
* Creates a {@code long} value from a sequence of ints. The conversion
* is made as if converting to a {@code long[]} array by filling the longs
* in little-endian order (least significant byte first), then combining
* all the longs with a xor operation.
*
* @param input Input bytes
* @return a {@code long}.
*/
static long intArray2Long(int[] input) {
long output = 0;

final int n = input.length;
// xor in the bits to a long in little-endian order
for (int i = 0; i < n; i++) {
// i = int index
// i >> 1 = long index
// i & 0x1 = int number in the long [0, 1]
// (i & 0x1) << 5 = little-endian byte shift to the long {0, 32}
output ^= (input[i] & 0xffffffffL) << ((i & 0x1) << 5);
}

return output;
}
}
Expand Up @@ -66,7 +66,7 @@ protected Integer convert(long[] seed, int size) {
}
@Override
protected Integer convert(byte[] seed, int size) {
return INT_ARRAY_TO_INT.convert(BYTE_ARRAY_TO_INT_ARRAY.convert(seed));
return Conversions.byteArray2Int(seed);
}
},
/** The seed type is {@code Long}. */
Expand All @@ -85,15 +85,15 @@ protected Long convert(Long seed, int size) {
}
@Override
protected Long convert(int[] seed, int size) {
return INT_TO_LONG.convert(INT_ARRAY_TO_INT.convert(seed));
return Conversions.intArray2Long(seed);
}
@Override
protected Long convert(long[] seed, int size) {
return LONG_ARRAY_TO_LONG.convert(seed);
}
@Override
protected Long convert(byte[] seed, int size) {
return LONG_ARRAY_TO_LONG.convert(BYTE_ARRAY_TO_LONG_ARRAY.convert(seed));
return Conversions.byteArray2Long(seed);
}
},
/** The seed type is {@code int[]}. */
Expand Down
@@ -0,0 +1,143 @@
/*
* 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.simple.internal;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.IntStream;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

/**
* Tests for {@link Conversions}.
*/
class ConversionsTest {
/**
* Gets the lengths for the byte[] seeds to convert.
*
* @return the lengths
*/
static IntStream getByteLengths() {
return IntStream.rangeClosed(0, Long.BYTES * 2);
}

/**
* Gets the lengths for the int[] seeds to convert.
*
* @return the lengths
*/
static IntStream getIntLengths() {
return IntStream.rangeClosed(0, (Long.BYTES / Integer.BYTES) * 2);
}

@ParameterizedTest
@MethodSource(value = {"getIntLengths"})
void testByteArray2Int(int bytes) {
final byte[] seed = new byte[bytes];
ThreadLocalRandom.current().nextBytes(seed);

// byte[] -> int[] -> int
// Concatenate all bytes in little-endian order to bytes
final int outLength = SeedUtils.intSizeFromByteSize(bytes);
final byte[] filledSeed = Arrays.copyOf(seed, outLength * Integer.BYTES);
final ByteBuffer bb = ByteBuffer.wrap(filledSeed)
.order(ByteOrder.LITTLE_ENDIAN);
// xor all the bytes read as ints
int expected = 0;
for (int i = outLength; i-- != 0;) {
long l = bb.getInt();
expected ^= l;
}

Assertions.assertEquals(expected, Conversions.byteArray2Int(seed));
}

@ParameterizedTest
@MethodSource(value = {"getIntLengths"})
void testByteArray2IntComposed(int bytes) {
final byte[] seed = new byte[bytes];
ThreadLocalRandom.current().nextBytes(seed);
final int expected = new IntArray2Int().convert(new ByteArray2IntArray().convert(seed));
Assertions.assertEquals(expected, Conversions.byteArray2Int(seed));
}

@ParameterizedTest
@MethodSource(value = {"getIntLengths"})
void testByteArray2Long(int bytes) {
final byte[] seed = new byte[bytes];
ThreadLocalRandom.current().nextBytes(seed);

// byte[] -> long[] -> long
// Concatenate all bytes in little-endian order to bytes
final int outLength = SeedUtils.longSizeFromByteSize(bytes);
final byte[] filledSeed = Arrays.copyOf(seed, outLength * Long.BYTES);
final ByteBuffer bb = ByteBuffer.wrap(filledSeed)
.order(ByteOrder.LITTLE_ENDIAN);
// xor all the bytes read as longs
long expected = 0;
for (int i = outLength; i-- != 0;) {
long l = bb.getLong();
expected ^= l;
}

Assertions.assertEquals(expected, Conversions.byteArray2Long(seed));
}

@ParameterizedTest
@MethodSource(value = {"getIntLengths"})
void testByteArray2LongComposed(int bytes) {
final byte[] seed = new byte[bytes];
ThreadLocalRandom.current().nextBytes(seed);
final long expected = new LongArray2Long().convert(new ByteArray2LongArray().convert(seed));
Assertions.assertEquals(expected, Conversions.byteArray2Long(seed));
}

@ParameterizedTest
@MethodSource(value = {"getIntLengths"})
void testIntArray2Long(int ints) {
final int[] seed = ThreadLocalRandom.current().ints(ints).toArray();

// int[] -> long[] -> long
// Concatenate all ints in little-endian order to bytes
final int outLength = SeedUtils.longSizeFromIntSize(ints);
final int[] filledSeed = Arrays.copyOf(seed, outLength * 2);
final ByteBuffer bb = ByteBuffer.allocate(filledSeed.length * Integer.BYTES)
.order(ByteOrder.LITTLE_ENDIAN);
Arrays.stream(filledSeed).forEach(bb::putInt);
// xor all the bytes read as longs
long expected = 0;
bb.flip();
for (int i = outLength; i-- != 0;) {
long l = bb.getLong();
expected ^= l;
}

Assertions.assertEquals(expected, Conversions.intArray2Long(seed));
}

@ParameterizedTest
@MethodSource(value = {"getIntLengths"})
void testIntArray2LongComposed(int ints) {
final int[] seed = ThreadLocalRandom.current().ints(ints).toArray();
final long expected = new LongArray2Long().convert(new IntArray2LongArray().convert(seed));
Assertions.assertEquals(expected, Conversions.intArray2Long(seed));
}
}

0 comments on commit d53f3da

Please sign in to comment.