Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
RNG-169: Avoid intermediate arrays during conversions
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
Showing
6 changed files
with
620 additions
and
8 deletions.
There are no files selected for viewing
107 changes: 107 additions & 0 deletions
107
commons-rng-simple/src/main/java/org/apache/commons/rng/simple/internal/Conversions.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
143 changes: 143 additions & 0 deletions
143
commons-rng-simple/src/test/java/org/apache/commons/rng/simple/internal/ConversionsTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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)); | ||
} | ||
} |
Oops, something went wrong.