Time to make jrand48 and friends available again?

The reason I mentioned upthread that a seedable random number generator is one of my top wishes for the standard library is pretty much this: I am not an expert in this area either.

Randomized testing (property-based, metamorphic, etc) is an extremely important test strategy and should be supported directly by Swift without requiring personal expertise in random number generators or a 3rd party library.

5 Likes

To add another voice to this chorus, I’m about as close to an expert as a non-expert can get.

I have a degree in math, I’ve been reading about RNGs (and studying their implementations) as a hobby for years. I’m currently in the process of rewriting the Swift standard library’s floating-point random number generator to do what it says on the tin (generate any value in the range with correct probability). Just today I wrote a proof-of-concept reimplementation of RandomNumberGenerator.next(upperBound:) -> T that’s 10-20% faster than the standard library’s version for the hardest cases, and equal speed for the general case.

And despite all of that, I do not know how to properly choose an RNG.

I have a general hazy feeling that Xoroshiro256++ (or **) is probably where one wants to be for most non-cryptographic applications, but I can’t back that up with any solid rationale.

Now, I don’t necessarily need a repeatable RNG to be in the standard library, but it would certainly be nice to have a generally-agreed-on-by-experts option available. Putting it in the Numerics package would also work just fine for me.

2 Likes

Certainly non-catastrophic ones exist. The entropy in the low-order bits is prone to be not very good, but that can be fine, depending on use. PCG, which is one of the families that I consider to be worth using, pairs a simple LCG with an "output function" to overcome this limitation (jrand48 effectively does, too, but with a much less good output function--dropping the low-order 16 bits--but it's still better than nothing). The nice thing about this approach is that the only dependency between iterations is the LCG, so on a modern wide core, you get equivalent performance to a simple LCG, but with better statistical properties.

To give you a little reassurance, based on what you've told me about your use case, what you're doing is fine for now.

@nevin: yeah, the recent Xoroshiro variants and PCG-family are the two most well-known "pretty good choices" (largely because of the ongoing spat between their creators--no such thing as bad publicity :joy:).

1 Like

And yet, where are they, for Swift? Please consider your distaste for jrand48 as a motivator. ;-)

1 Like

Yeah, just to be totally clear, I'm definitely not saying "there's nothing to be done here." There's a very real and very important feature that you're asking for.

FWIW, the PCG Paper looks fascinating. I wish I had about a week to read it!

2 Likes

I have an implementation of Xoroshiro256** in Swift, but I'm not sure if it's ready for public consumption. I haven't even bothered commenting it yet. If it'll help you, here's the stripped down bare to minimum version:

Xoroshiro256StarStar.swift
private func rotl(_ x: UInt64, _ k: UInt64) -> UInt64 {
    return (x << k) | (x >> (64 - k))
}

struct Xoroshiro256StarStar: RandomNumberGenerator {
    typealias State = (UInt64, UInt64, UInt64, UInt64)
    var state: State

    init<Source: RandomNumberGenerator>(from source: inout Source) {
        repeat {
            state = (source.next(), source.next(), source.next(), source.next())
        } while state == (0, 0, 0, 0)
    }

    init() {
        var entropy = SystemRandomNumberGenerator()
        self.init(from: &entropy)
    }

    init?(state: State) {
        guard state != (0, 0, 0, 0) else { return nil }
        self.state = state
    }

    mutating func next() -> UInt64 {
        let result = rotl(state.1 &* 5, 7) &* 9
        let t = state.1 << 17

        state.2 ^= state.0
        state.3 ^= state.1
        state.1 ^= state.2
        state.0 ^= state.3

        state.2 ^= t
        state.3 = rotl(state.3, 45)

        return result
    }
}

I'm interested, whether stdlib PRNG be required to generate the same stream on all platforms?

Also let me plug my PCG package here.

1 Like

Yes, I would expect any seedable RNG added to the standard lib to generate a fully-portable stream when given identical seed.

1 Like

I’ll second this. I’ve used Matt Gallagher’s RNGs when needed in the past, but I think this is the kind of thing that should be in the library. Even if we choose a not-particularly-good generator (and say so in the docs), anything with seed capabilities would help already.

1 Like