[Pitch] An API for Bulk Random Bytes (again)

The previous pitch was never made into a proposal largely because we were waiting on a safe partially uninitialized buffer type. Now that we have such a type in OutputRawSpan, now might be a good time to consider adding such an API to RandomNumberGenerator.

Also, and this may require a seperate proposal, I think we should extend RandomNumberGenerator to support non-copyable types.

My proposed API is very similar to the previous pitch:

// Initializes the remaining free capacity of the buffer with random bytes.
mutating func initialize(_ buffer: inout OutputRawSpan)
// Fills the entirety of the buffer with random bytes.
mutating func fill(_ buffer: inout MutableRawSpan)

These methods would be required to draw from the same random stream as next() -> UInt64

This would include the obvious implementations for SystemRandomNumberGenerator, as well as less obvious default implementations for use in other RandomNumberGenerators.

public struct SystemRandomNumberGenerator: RandomNumberGenerator {
  // ...
  public mutating func initialize(_ buffer: inout OutputRawSpan) {
    let count = bytes.freeCapacity
    buffer.withUnsafeMutableBytes { buffer, initializedCount in
      guard let addr = buffer.baseAddress else { return }
      swift_stdlib_random(addr + initializedCount, count)
      initializedCount &+= count
    }
  }
  mutating func fill(_ buffer: inout MutableRawSpan) {
    buffer.withUnsafeMutableBytes { buffer in
        guard let addr = buffer.baseAddress else { return }
        swift_stdlib_random(addr, buffer.count)
    }
  }
}

extension RandomNumberGenerator {
  // ...
  public mutating func initialize(_ buffer: inout OutputRawSpan) {
    while buff.freeCapacity >= MemoryLayout<UInt64>.size {
      buffer.append(next(), as: UInt64.self)
    }
    if buffer.isFull { return }
    // Many PRNGs are more random in their high bytes, so we'll
    // prefer them over the low ones.
    withUnsafeBytes(of: next().bigEndian) { block in
      for i in 0..<buffer.freeCapacity {
        buffer.append(block[i], as: UInt8.self)
      }
    }
  }
  public mutating func fill(_ buffer: inout MutableRawSpan) {
    let blockSize = MemoryLayout<UInt64>.size
    let (count, remainder) = buffer.count.quotientAndRemainder(dividingBy: blockSize)
    for i in stride(from: 0, to: count, by: blockSize) {
      buffer.storeBytes(of: next(), toByteOffset: i, as: UInt64.self)
    }
    if remainder == 0 { return }
    // Many PRNGs are more random in their high bytes, so we'll
    // prefer them over the low ones.
    withUnsafeBytes(of: next().bigEndian) { block in
      for i in 0..<remainder {
        buffer.storeBytes(of: block[i], toByteOffset: count &+ i, as: UInt8.self)
      }
    }
  }
}

Future directions:

swift_stdlib_random isn't neccessarilly efficient for large bulk operations, but I believe it's implementation can be improved separately and without an evolution proposal. The current implementation should at the very least be suitable for use in seeding pseudorandom number generators.

ConvertibleFromRawBytes will allow us to write a safe zero-copy random factory function for arbitrary types.

extension ConvertibleFromRawBytes {
  public static func random(using rng: inout RandomNumberGenerator) -> Self {
    withUnsafeTemporaryAllocation(of: Self.self, capacity: 1) { buffer in
      let bytes = UnsafeMutableRawBufferPointer(buffer)
      var span = OutputRawSpan(buffer: bytes, initializedCount: 0)
      rng.initialize(&span)
      return buffer.moveElement(from: 0)
    }
  }
  public static func random() -> Self {
    var rng = SystemRandomNumberGenerator()
    return random(using: &rng)
  }
}

This can be made even simpler with OutputSpan versions of withTemporaryAllocation

PS. There's probably a very obvious way to extract bytes from next() highest to lowest without using withUnsafeBytes, but I'm having trouble thinking of it.

Alternatives considered:

Only having a method that takes an OutputRawSpan. While a method that doesn't need to track which bytes are initalized is potentially more efficient, one can always convert a MutableRawSpan to an OutputRawSpan if you're willing to launder it through UnsafeMutableRawBufferPointer.

6 Likes

I wonder if there’s value in giving RNGs a way to indicate their “natural” or “preferred” buffer size.

1 Like

Automatic conformance on ConvertibleFromRawBytes is problematic with types like Float, for which random bit patterns correspond to non-uniformly random values, when they correspond to a numeric value at all. I think this is best left to numeric types to implement, and if you want a wholly random ConvertibleFromRawBytes instance, you can pull one out of a RawSpan yourself.

That aside, I think integrating MutableSpan into the RNG protocols is something that should go forward.

Right, we would definitely not want to do this, for exactly the reason Felix gives.

1 Like