From 71c0d508f7cece5ec1aacdb639b80c23595b02a9 Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Sun, 9 Mar 2025 11:26:15 +0700 Subject: [PATCH] Provide JSF32, JSF64 and WOB2M random generators --- gradle.properties | 2 +- .../dbotthepony/kommons/random/JSF32Random.kt | 58 ++++++++++++++++++ .../dbotthepony/kommons/random/JSF64Random.kt | 57 +++++++++++++++++ .../dbotthepony/kommons/random/WOB2MRandom.kt | 61 +++++++++++++++++++ 4 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/ru/dbotthepony/kommons/random/JSF32Random.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kommons/random/JSF64Random.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kommons/random/WOB2MRandom.kt diff --git a/gradle.properties b/gradle.properties index 47c392b..a62a894 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ kotlin.code.style=official specifyKotlinAsDependency=false projectGroup=ru.dbotthepony.kommons -projectVersion=3.3.2 +projectVersion=3.4.0 guavaDepVersion=33.0.0 gsonDepVersion=2.8.9 diff --git a/src/main/kotlin/ru/dbotthepony/kommons/random/JSF32Random.kt b/src/main/kotlin/ru/dbotthepony/kommons/random/JSF32Random.kt new file mode 100644 index 0000000..7e9283c --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kommons/random/JSF32Random.kt @@ -0,0 +1,58 @@ +package ru.dbotthepony.kommons.random + +import java.util.random.RandomGenerator + +/** + * This is a small fast pseudorandom number generator, suitable for large statistical calculations, but not of cryptographic quality. + * Although there is no guaranteed minimum cycle length, the average cycle length is expected to be about 2^126 results. + * + * [https://burtleburtle.net/bob/rand/smallprng.html](https://burtleburtle.net/bob/rand/smallprng.html) + */ +open class JSF32Random protected constructor( + @JvmField + protected var s0: Int, + @JvmField + protected var s1: Int, + @JvmField + protected var s2: Int, + @JvmField + protected var s3: Int, + marker: Nothing? +) : RandomGenerator { + /** + * Preferred way to initialize this PRNG. + * As pointed out by Melissa O'Neill, Bob Jenkins did this to avoid short cycles, + * because if PRNG is allowed to be seeded in its entirety, then it is likely to fall on edge case and have short cycle, + * (and such seeds / internal states have been found, some examples + * + * see [https://burtleburtle.net/bob/rand/smallprng.html](https://burtleburtle.net/bob/rand/smallprng.html) and + * [https://www.pcg-random.org/posts/bob-jenkins-small-prng-passes-practrand.html](https://www.pcg-random.org/posts/bob-jenkins-small-prng-passes-practrand.html) + */ + constructor(seed: Int) : this(0xf1ea5eed.toInt(), seed, seed, seed, null) { + for (i in 0 until 20) + nextInt() + } + + final override fun nextInt(): Int { + val e = s0 - s1.rotateLeft(27) + s0 = s1 xor s2.rotateLeft(17) + s1 = s2 + s3 + s2 = s3 + e + s3 = e + s0 + return s3 + } + + override fun nextLong(): Long { + val a = nextInt().toLong() + val b = nextInt().toLong() and 0xFFFFFFFFL + return a.shl(32) or b + } + + companion object { + @JvmStatic + @Deprecated("JSF suffers from 'weak seed' weakness, using raw initialization is highly discouraged, unless serializing/deserializing") + fun raw(s0: Int, s1: Int, s2: Int, s3: Int): JSF32Random { + return JSF32Random(s0, s1, s2, s3, null) + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kommons/random/JSF64Random.kt b/src/main/kotlin/ru/dbotthepony/kommons/random/JSF64Random.kt new file mode 100644 index 0000000..fc55b9c --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kommons/random/JSF64Random.kt @@ -0,0 +1,57 @@ +package ru.dbotthepony.kommons.random + +import java.util.random.RandomGenerator + +/** + * 64-bit version of [JSF32Random] generator. + * + * While it should have better runtime properties (and better statistical properties due + * to larger period), Bob Jenkins not sure himself whenever this 64-bit variant is correct, as stated on his website: + * + * > If you want to use 8-byte terms instead of 4-byte terms, the proper rotate amounts are (39,11) for the two-rotate version (yielding at least 13.3 bits of avalanche after 5 rounds) or (7,13,37) for the three-rotate version (yielding 18.4 bits of avalanche after 5 rounds). + * > I think I'd go with the three-rotate version, because the ideal is 32 bits of avalanche, and 13.3 isn't even half of that. + * > I don't think that there's an 8-byte rotate instruction on any 64-bit platform. And you only need 2 terms to get to 128 bits of internal state if you have 64-bit terms. Quite likely 64-bit deserves a whole different approach, not just different constants. + * @see JSF32Random + */ +open class JSF64Random( + @JvmField + protected var s0: Long, + @JvmField + protected var s1: Long, + @JvmField + protected var s2: Long, + @JvmField + protected var s3: Long, + marker: Nothing? +) : RandomGenerator { + /** + * Preferred way to initialize this PRNG. + * As pointed out by Melissa O'Neill, Bob Jenkins did this to avoid short cycles, + * because if PRNG is allowed to be seeded in its entirety, then it is likely to fall on edge case and have short cycle, + * (and such seeds / internal states have been found, some examples + * + * see [https://burtleburtle.net/bob/rand/smallprng.html](https://burtleburtle.net/bob/rand/smallprng.html) and + * [https://www.pcg-random.org/posts/bob-jenkins-small-prng-passes-practrand.html](https://www.pcg-random.org/posts/bob-jenkins-small-prng-passes-practrand.html) + */ + constructor(seed: Long) : this(0xf1ea5eedL, seed, seed, seed, null) { + for (i in 0 until 20) + nextLong() + } + + final override fun nextLong(): Long { + val e = s0 - s1.rotateLeft(7) + s0 = s1 xor s2.rotateLeft(13) + s1 = s2 + s3.rotateLeft(37) + s2 = s3 + e + s3 = e + s0 + return s3 + } + + companion object { + @JvmStatic + @Deprecated("JSF suffers from 'weak seed' weakness, using raw initialization is highly discouraged, unless serializing/deserializing") + fun raw(s0: Long, s1: Long, s2: Long, s3: Long): JSF64Random { + return JSF64Random(s0, s1, s2, s3, null) + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kommons/random/WOB2MRandom.kt b/src/main/kotlin/ru/dbotthepony/kommons/random/WOB2MRandom.kt new file mode 100644 index 0000000..fbfda8b --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kommons/random/WOB2MRandom.kt @@ -0,0 +1,61 @@ +package ru.dbotthepony.kommons.random + +import java.util.random.RandomGenerator + +/** + * Disclaimer written by author of this PRNG, Bob Jenkins: + * > WARNING: Alpha code. I'm not happy with the amount of avalanche, or with it using a multiplication. + * > This is indefinitely in alpha until I have time to explore this space properly. + * > I may get a better idea and replace these and still call them the same name. + * > In the meantime you are encouraged to time it and detect bias in it. + * > It's faster than JFS64 on my home machine, but slower on my work machine. + * > The difference seems to be that my work machine has a slow multiply. + * > Chips with slow multiplies are very common. + * + * WOB2M (Wrangler Of Bits, 2 mixing variables, with Multiply) is a 64-bit noncryptographic pseudorandom number generator. + * It uses 1 counter and 2 mixing variables. It is reversible, but does not have the ability to jump forward or back by arbitrary amounts. + * All 1-mix 1-counter functions were too slow, so there will be no WOB or WOB1 or WOB1M. WOB2, WOB3, WOB3M are likely in the future. + * + * Its cycle length is guaranteed to be at least 2^64 values because it takes that long for the counter to wrap around. + * Every seed produces a distinct sequence for at least 2^64 values, because every seed starts with the same counter value but different other state, + * so it takes at least 2^64 values before the counter wraps around and the rest of the state gets a chance to collide with the initial state + * of some other seed. The longest cycle is expected to be of length 2^191, containing roughly half of all possible states, and the average cycle + * length is expected to be 2^190. Two arbitrary seeds will land on the same cycle with probability about 1/3, but that is OK because the sequences + * won't overlap for at least 2^64 values. + */ +open class WOB2MRandom protected constructor( + @JvmField + protected var seed0: Long, + @JvmField + protected var seed1: Long, + @JvmField + protected var count: Long, + marker: Nothing? +) : RandomGenerator { + constructor(seed0: Long, seed1: Long) : this(seed0, seed1, Long.MIN_VALUE + 10, null) { + for (i in 0 until 10) + nextLong() + } + + final override fun nextLong(): Long { + val temp = seed0 + count++ + seed0 = seed1 + temp.rotateLeft(12) + seed1 = (0x0581af43eb71d8b3 * temp) xor seed0.rotateLeft(28) + return seed1 + } + + fun previousLong(): Long { + val temp = 0x6cc3621b095c967b * (seed1 xor seed0.rotateLeft(28)) + seed1 = seed0 - temp.rotateLeft(12) + seed0 = temp - --count + return seed1 + } + + companion object { + @JvmStatic + @Deprecated("Quality of manual seeding is unknown (and probably very, very bad!), so don't use this unless serializing/deserializing") + fun raw(seed0: Long, seed1: Long, counter: Long): WOB2MRandom { + return WOB2MRandom(seed0, seed1, counter, null) + } + } +}