6

I have used Int.random() method and arc4random_uniform() for number generation speed tests.
Both tests were run in macOS console with build configuration set to release. Below are codes which I have used for testing.

public func randomGen1() {
    let n = 1_000_000
    let startTime = CFAbsoluteTimeGetCurrent()
    for i in 0..<n {
        _ = arc4random_uniform(10)
    }
    let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime
    print(timeElapsed)
}
public func randomGen2() {
    let n = 1_000_000
    let startTime = CFAbsoluteTimeGetCurrent()
    for i in 0..<n {
        _ = Int.random(in: 0..<10)
    }
    let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime
    print(timeElapsed)
}

The times I got are
0.029475092887878418 (for arc4random_uniform(10))
0.20298802852630615 (for Int.random(in: 0..<10))

Why is Int.random() so much slower?
Is there a way to optimise it?
Are there any faster methods for random number generation in swift?

AamirR
  • 11,672
  • 4
  • 59
  • 73
Adam Linke
  • 81
  • 6
  • 2
    What optimizer settings were you using? – Alexander Apr 26 '19 at 17:32
  • 3
    Looking at the code in the profiler, it seems that `Int.random(in:)` is using `arc4random_buf` internally which appears to be a lot slower than the `arc4random` that `arc4random_uniform` uses. – rmaddy Apr 26 '19 at 17:53
  • I don't know if that explains it, but from a quick look at https://github.com/apple/swift/blob/master/stdlib/public/core/Random.swift it seems that each call to `next(upperBound:)` computes a 64-bit random number (no matter what the upper bound is), and that calls (as @rmaddy observed) `arc4random_buf` with an 8 byte buffer on Apple platforms. – Martin R Apr 26 '19 at 18:05
  • Yep. `random(in:)` uses the `SystemRandomNumberGenerator`, which calls `swift_stdlib_random`, which calls `arc4random_buf`. – Rob Apr 26 '19 at 18:09
  • 1
    To be fair `arc4random_uniform()` produces a `UInt32`, and thus it should be compared to `UInt32.random(in:)` – ielyamani Apr 26 '19 at 18:25
  • 3
    @ielyamani: I tried that and it makes no difference. Both Int.random() and UInt32.random() compute a 64-bit random number and then “truncate” the result to the desired range. – Martin R Apr 26 '19 at 18:31
  • @MartinR In my tests too, I just mentioned that it should be taken into consideration. If implemented properly there should be a difference. – ielyamani Apr 26 '19 at 18:36
  • @Alexander I am using [-Os] optimization level. – Adam Linke Apr 26 '19 at 22:20
  • 1
    Never use `CFAbsoluteTimeGetCurrent`: it's a Wall clock, affected by leap seconds in your calendar. Instead, use a monotonic clock: `mach_absolute_time()`, `ProcessInfo.processInfo.systemUptime`, `DispatchTime.now()` or `CACurrentMediaTime()`. – Cœur May 29 '19 at 00:03
  • See the update – ielyamani Jul 23 '19 at 21:02

1 Answers1

2

Update

This implementation of a random number generator in an interval has been merged into the standard library and should perform better than before:

// s = upperBound; r1, r2 = random numbers from generator
func bounded(s: UInt64, r1:UInt64, r2: UInt64) -> UInt64 {
    // r1 would come from invoking generator's next()
    var m = r1.multipliedFullWidth(by: s)
    if m.low < s {
        // let t = (0 &- s) % s // Lemire's original form
        var t = 0 &- s // O'Neill's modulo optimization
        if t >= s {
            t &-= s
            if t >= s {
                t %= s
            }
        }
        while m.low < t {
            // r2 would come from invoking generator's next()
            m = r2.multipliedFullWidth(by: s)
        }
    }
    return m.high
}

See the answer below for more details.

Answer

An answer to your second question :

"Are there any faster methods for random number generation in swift?"

I've previously used the Xoshiro Pseudo-Random Number Generator which is pretty fast.

Here the code used for benchmarking :

  • randomGen1
import Foundation

public func randomGen1() {
    let n = 1_000_000
    var sum: UInt32 = 0
    let startTime = CFAbsoluteTimeGetCurrent()
    for _ in 0..<n {
        sum = sum &+ arc4random_uniform(10)
    }
    let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime
    print(sum, timeElapsed)
}

do {
    randomGen1()
}
  • randomGen2
public func randomGen2() {
    let n = 1_000_000
    var sum: UInt32 = 0
    let startTime = CFAbsoluteTimeGetCurrent()
    for _ in 0..<n {
        sum = sum &+ UInt32.random(in: 0..<10)
    }
    let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime
    print(sum, timeElapsed)
}


do {
    randomGen2()
}
struct Xoshiro: RandomNumberGenerator {
    public typealias StateType = (UInt32, UInt32, UInt32, UInt32)

    private var state: StateType

    public init(seed: StateType) {
        self.state = seed
    }

    public mutating func next() -> Int {
        let x = state.1 &* 5
        let result = ((x &<< 7) | (x &>> 25)) &* 9
        let t = state.1 &<< 9
        state.2 ^= state.0
        state.3 ^= state.1
        state.1 ^= state.2
        state.0 ^= state.3
        state.2 ^= t
        state.3 = (state.3 &<< 21) | (state.3 &>> 11)
        return Int(result)
    }
}

var x = Xoshiro(seed: (UInt32.random(in: 0..<10),  //Other upper limits could be used to increase randomness
    UInt32.random(in: 0..<10),
    UInt32.random(in: 0..<10),
    UInt32.random(in: 0..<10)))

public func randomGen3() {
    let n = 1_000_000
    var sum: UInt32 = 0
    let startTime = CFAbsoluteTimeGetCurrent()
    for _ in 0..<n {
        sum = sum &+ UInt32(abs(x.next()) % 10)
    }
    let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime
    print(sum, timeElapsed)
}

do {
    randomGen3()
}

Xoshiro is fast but does not pass all randomness tests. If security is of concern then you could use Wyhash.

Daniel Lemire (the author of this paper) has kindly just sent me a Swift implementation of Wyhash:

class WyhashGenerator {
    var seed : UInt64

    let multiplier1 : UInt64 = 0xa3b195354a39b70d
    let multiplier2 : UInt64 = 0x1b03738712fad5c9
    let increment : UInt64 = 0x60bee2bee120fc15

    init(userSeed : UInt64) {
        seed = userSeed;
    }

    func random() -> UInt64 {
        seed &+= increment
        let fullmult1 = seed.multipliedFullWidth(by: multiplier1)
        let m1 = fullmult1.high ^ fullmult1.low;
        let fullmult2 = m1.multipliedFullWidth(by: multiplier2)
        let m2 = fullmult2.high ^ fullmult2.low;
        return m2
    }
}

It can be used like so:

public func randomGen4() {
    let n = 1_000_000
    var sum: UInt64 = 0
    let startTime = CFAbsoluteTimeGetCurrent()
    let gen = WyhashGenerator(userSeed: 0)
    for _ in 0..<n {
        sum = sum &+ gen.random() % 10
    }
    let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime
    print(sum, timeElapsed)
}

do {
    randomGen4()
}

And here are the benchmark results, with the code compiled in the terminal with optimizations (-O) :

arc4random_uniform()  : 0.034s
UInt32.random(in:)    : 0.243s
WyHash64              : 0.002s
Xoshiro               : 0.001s

You can find more random number generators here.

ielyamani
  • 17,807
  • 10
  • 55
  • 90
  • 8
    Please be very thoughtful when switching from `SystemRandomNumberGenerator` to Xoshiro. The former is a cryptographically secure PRNG whenever possible. Xoshiro is not a cryptographically secure PRNG. That's the kind of thing that doesn't matter until it suddenly matters quite a lot. The security of a CSPRNG has a performance cost, so any decent non-CSPRNG should be much faster. – Rob Napier Apr 26 '19 at 18:38
  • 4
    Note that a RandomNumberGenerator must implement `public mutating func next() -> UInt64` – and then you can call `Int.random(in: 0..<10, using: &x)` instead of modulo arithmetic (which suffers from the “modulo bias” problem). – Martin R Apr 26 '19 at 20:57
  • Btw, that a RNG compiles without that required method has been observed here: https://forums.swift.org/t/trying-to-use-the-new-randomnumbergenerator-protocol/12919. – Martin R Apr 26 '19 at 21:03
  • 1
    @ielyamani I don't believe Big Crush is a sufficient test for a CSPRNG (cryptographically secure PRNG). Big Crush is just a test of statistical randomness. That's is substantially less than a CSPRNG promises. For example, it is required that it be impractical (i.e. exceeds polynomial time) to determine a CSPRNG's previous outputs, even if you know its full current internal state. Passing basic statistical tests are table stakes in any PRNG, but no statistical test can prove a CSPRNG. – Rob Napier May 28 '19 at 20:44
  • Wikipedia's CSPRNG article isn't a great intro to the subject IMO, but it does include a nice example of a good PRNG that is completely broken cryptographically. Generate a seed and use it to select a digit of pi. For each iteration, output the next digit. That's perfectly fine for generating a random sequence that will pass statistical tests, but if you know which digit the PRNG is currently computing (i.e. the internal state of the PRNG), it's trivial to determine all the previous digits that were output. https://en.wikipedia.org/wiki/Cryptographically_secure_pseudorandom_number_generator – Rob Napier May 28 '19 at 20:49
  • See first comment. It is important that people know whether they're using a secure or insecure PRNG. There's nothing fundamentally wrong with insecure PRNGs. But there are many places that random numbers lead to security issues having nothing to do with encryption. "That's the kind of thing that doesn't matter until it suddenly matters quite a lot." Passing Big Crush has no bearing on the security implications of the choice. – Rob Napier May 28 '19 at 21:22
  • Never use `CFAbsoluteTimeGetCurrent`: it's a Wall clock, affected by leap seconds in your calendar. Instead, use a monotonic clock: `mach_absolute_time()`, `ProcessInfo.processInfo.systemUptime`, `DispatchTime.now()` or `CACurrentMediaTime()`. – Cœur May 29 '19 at 01:12
  • @ielyamani I've been aware of that question for nearly 4 years: see nevyn answer and the comments under it. ;) – Cœur May 29 '19 at 01:34