Skip to content

Instantly share code, notes, and snippets.

@nicklockwood
Created May 17, 2020 02:21
Show Gist options
  • Save nicklockwood/cf1fde3eb24602883c17f94e63e490bc to your computer and use it in GitHub Desktop.
Save nicklockwood/cf1fde3eb24602883c17f94e63e490bc to your computer and use it in GitHub Desktop.
RNG not working as expected
struct RNG: RandomNumberGenerator {
let modulus: UInt64 = 233_280
let multiplier: UInt64 = 9301
let increment: UInt64 = 49297
var seed: UInt64 = 0
mutating func next() -> UInt64 {
seed = (seed * multiplier + increment) % modulus
return seed
}
}
var rng = RNG(seed: 1000)
var numbers = [0, 1, 2, 3, 4, 5, 6]
print("### next() ###")
for _ in 0 ..< 5 {
print(rng.next())
}
print("### next(upperBound:) ###")
for _ in 0 ..< 5 {
print(rng.next(upperBound: UInt(numbers.count)))
}
print("### random(in:) ###")
for _ in 0 ..< 5 {
print(numbers[Int.random(in: numbers.indices, using: &rng)])
}
print("### randomElement() ###")
for _ in 0 ..< 5 {
print(numbers.randomElement(using: &rng)!)
}
@nicklockwood
Copy link
Author

Output:

### next() ###
19097
144414
17671
178148
16005
### next(upperBound:) ###
0
0
0
0
0
### random(in:) ###
0
0
0
0
0
### randomElement() ###
0
0
0
0
0

@tempelmann
Copy link

When I run this in Xcode 10.1 I get the expected results:

### next() ###
19097
144414
17671
178148
16005
### next(upperBound:) ###
6
3
0
1
5
### random(in:) ###
6
0
4
3
1
### randomElement() ###
5
5
4
1
3

@nicklockwood
Copy link
Author

@tempelmann it works for me too in Xcode 11.3.1.

I think this was a regression, possibly introduced by swiftlang/swift#25286

This is very frustrating, because it's a pretty standard expectation that an RNG will use the low order bits when selecting a number in a small range, and while using the full 64-bit range may be more correct by some mathematical standard, it's a huge breaking change to have implemented with little or no fanfare.

@tempelmann
Copy link

tempelmann commented May 17, 2020

Actually, if you look at https://en.wikipedia.org/wiki/Linear_congruential_generator, at the table column "output bits of seed", you'll see that many PRNGs have the useable bits in the upper area. That's why this Swift class should have a function that returns that "useable range", e.g. as a divisor and a max value, and the upperbound function should use that info to normalize the result from next() first, before then applying the reduction for upperBound. It's generally unlikely to expect that all 64 bits of even the standard (system) RNG are all useable.

Also, I find the name "upperBound" misleading. Is that term used throughout Swift for "max value plus + 1"? To me, upper bound means max value, without plus 1.

@nicklockwood
Copy link
Author

The upperBound usage is consistent with the rest of Swift.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment