Speedy Random Numbers In Swift 4.2

This post will dive into generating random numbers in Swift 4.2 on iOS and macOS, using Instruments to profile performance. If you are not familiar with Instruments (part of Xcode) check out this Instruments Overview.

Generating Random Numbers In Swift 4.2

Swift 4.2 provides convenient interfaces for obtaining random numbers and random elements of collections:

// Random integer in the range [0, 10)

// Random integer in range [0, 2]
[0, 1, 2].randomElement()

Behind the scenes, certain types and collections adhere to the RandomNumberGenerator protocol in the Swift standard library. On iOS and macOS the implementation of RandomNumberGenerator relies on calling the _stdlib_random function defined in LibcShims.cpp:

void swift::_stdlib_random(void *buf, __swift_size_t nbytes) {
  arc4random_buf(buf, nbytes);

The function arc4random_buf(void *buf, size_t nbytes) generates nbytes of randomness and writes the random bytes to buffer buf. Knowing the trace and underlying root function we can look at the performance characteristics for random number generation.

Random Number Generator Performance

While looking into the performance of IndexSet.union, a performance trace from Instruments contained an unexpected result. The code profiled in this trace repeatedly inserted numbers into an IndexSet a random number of times.

Time Weight Symbol
6.00ms 20.0% swift_retain(swift::HeapObject*)
4.00ms 13.3% static UInt._random(using:)
4.00ms 13.3%     static Int._random(using:)
4.00ms 13.3%         protocol witness for RandomNumberGenerator._fill(bytes:)
4.00ms 13.3%             arc4random_buf
2.00ms 6.6% IndexSet.insert(_:)
2.00ms 6.6%     NSMutableIndexSet.add(_:)
2.00ms 6.6% swift_release(swift::HeapObject*)

The odd part is the amount of compute time spent on arc4random_buf versus NSMutableIndexSet.add(_:). Naively, we might assume adding into an index set takes more compute than generating a random numbers.

Upon further profiling, the results indicate arc4random_buf is slower than arc4random and arc4random_uniform. Using this information we can construct more performant alternatives to the existing Swift interfaces:

// Faster implementation to get a random index in a collection
let index = arc4random_uniform(collection.count)

// Faster implementation to get a 64-bit random number
let value1: UInt32 = arc4random()
let value2: UInt32 = arc4random()
let random = (UInt64(value1) << 32 | UInt64(value2))

On the machine used for profiling, combining two 32-bit numbers was over 500% faster than using arc4random_buf.

Note: if generating random numbers securely is a relevant concern to your application consider using SecRandomCopyBytes.

If your application relies on random numbers, especially for graphs or game related computation, consider looking into the different performance characteristics of the available randomness functions on the platforms your application supports. You may be able to achieve a meaningful performance improvement by optimizing random number generation.